Best practice su if / return


60

Voglio sapere quale è considerato il modo migliore di tornare quando ho una ifdichiarazione.

Esempio 1:

public bool MyFunction()
{
   // Get some string for this example
   string myString = GetString();

   if (myString == null)
   {
      return false;
   }
   else
   {
      myString = "Name " + myString;
      // Do something more here...
      return true;
   }
}

Esempio 2:

public bool MyFunction()
{
   // Get some string for this example
   string myString = GetString();

   if (myString == null)
   {
      return false;
   }

   myString = "Name " + myString;
   // Do something more here...
   return true;
}

Come puoi vedere in entrambi gli esempi, la funzione tornerà true/falsema è una buona idea inserire elseun'istruzione come nel primo esempio o è meglio non inserirla?


7
Se stai controllando solo gli errori nel primo 'if', è meglio non includere 'else' perché gli errori non dovrebbero essere considerati parte della logica effettiva.
Mert Akcakaya,

2
personalmente sarei più preoccupato per la funzione che causa effetti collaterali, ma immagino che questo sia solo un esempio mal scelto?
jk.


14
Per me, la prima versione è come respingendo tutti gli studenti nella stanza che non hanno terminato il loro lavoro, e poi una volta se ne sono andati, dicendo al resto degli studenti "Ora, se hai finito il tuo lavoro ...". Ha senso, ma non è necessario. Dato che il condizionale non è più un condizionale, tendo a lasciar perdere else.
Nicole,

1
Cos'è il "Fai qualcosa di più qui" che vuoi fare? Ciò potrebbe cambiare completamente il modo in cui progetti la tua funzione.
Benedetto

Risposte:


81

L'esempio 2 è noto come blocco di protezione. È più adatto a restituire / generare l'eccezione in anticipo se qualcosa è andato storto (parametro errato o stato non valido). Nel normale flusso logico è meglio usare l'Esempio 1


+1 - una buona distinzione e ciò a cui avrei risposto.
Telastyn,

3
@ pR0Ps ha la stessa risposta di seguito e fornisce un esempio di codice sul motivo per cui finisce per essere un percorso più pulito da seguire. +1 per la buona risposta.

Questa domanda è stata posta molte volte su SO e sebbene possa essere controversa questa è una buona risposta. Molte persone che sono state addestrate a tornare solo dalla fine hanno alcuni problemi con questo concetto.
Bill K,

2
+1. Evitare in modo inappropriato i blocchi di protezione tende a causare il codice freccia.
Brian

Entrambi gli esempi non mostrano il blocco di guardia?
Ernie,

44

Il mio stile personale è usare il singolo ifper i blocchi di guardia e if/ elsenel codice di elaborazione del metodo effettivo.

In questo caso, stai usando la myString == nullcondizione di guardia, quindi tenderei ad usare il singolo ifschema.

Considera il codice un po 'più complicato:

Esempio 1:

public bool MyFunction(myString: string){

    //guard block
    if (myString == null){
        return false;
    }
    else{
        //processing block
        myString = escapedString(myString);

        if (myString == "foo"){
            //some processing here
            return false;
        }
        else{
            myString = "Name " + myString;
            //other stuff
            return true;
        }
    }
}

Esempio 2:

public bool MyFunction(myString: string){

    //guard block
    if (myString == null){
        return false;
    }

    //processing block
    myString = escapedString(myString);

    if (myString == "foo"){
        //some processing here
        return false;
    }
    else{
        myString = "Name " + myString;
        //other stuff
        return true;
    }
}

Nell'esempio 1, sia la protezione che il resto del metodo sono nella forma if/ else. Confrontalo con l'esempio 2, in cui il blocco di protezione si trova nella ifforma singola , mentre il resto del metodo utilizza la forma if/ else. Personalmente, trovo l'esempio 2 più facile da capire, mentre l'esempio 1 sembra disordinato e troppo indentato.

Si noti che questo è un esempio inventato e che è possibile utilizzare le else ifistruzioni per ripulirlo, ma sto cercando di mostrare la differenza tra i blocchi di guardia e il codice di elaborazione della funzione effettiva.

Un compilatore decente dovrebbe comunque generare lo stesso output per entrambi. L'unico motivo per utilizzare l'una o l'altra è la preferenza personale o per conformarsi allo stile del codice esistente.


18
L'esempio 2 sarebbe ancora più facile da leggere se ti liberassi del secondo.
Briddums,

18

personalmente preferisco il secondo metodo. Sento che è più corto, ha meno rientri ed è più facile da leggere.


+1 per meno rientro. Questo gesto ovvio se hai più controlli
d.raev il

15

La mia pratica personale è la seguente:

  • Non mi piacciono le funzioni con diversi punti di uscita, ho trovato difficile da mantenere e seguire, a volte le modifiche al codice rompono la logica interna perché intrinsecamente un po 'sciatta. Quando si tratta di un calcolo complesso, creo un valore di ritorno all'inizio e lo restituisco alla fine. Questo mi costringe a seguire attentamente ogni percorso if-else, switch, etc, impostare correttamente il valore nelle posizioni appropriate. Dedico anche un po 'di tempo a decidere se impostare un valore di ritorno predefinito o lasciarlo non inizializzato all'inizio. Questo metodo aiuta anche quando cambia la logica o il tipo di valore restituito o il significato.

Per esempio:

public bool myFunction()
{
   // First parameter loading
   String myString = getString();

   // Location of "quick exits", see the second example
   // ...

   // declaration of external resources that MUST be released whatever happens
   // ...

   // the return variable (should think about giving it a default value or not) 
   // if you have no default value, declare it final! You will get compiler 
   // error when you try to set it multiple times or leave uninitialized!
   bool didSomething = false;

   try {
     if (myString != null)
     {
       myString = "Name " + myString;
       // Do something more here...

       didSomething = true;
     } else {
       // get other parameters and data
       if ( other conditions apply ) {
         // do something else
         didSomething = true;
       }
     }

     // Edit: previously forgot the most important advantage of this version
     // *** HOUSEKEEPING!!! ***

   } finally {

     // this is the common place to release all resources, reset all state variables

     // Yes, if you use try-finally, you will get here from any internal returns too.
     // As I said, it is only my taste that I like to have one, straightforward path 
     // leading here, and this works even if you don't use the try-finally version.

   }

   return didSomething;
}
  • L'unica eccezione: "uscita rapida" all'inizio (o in rari casi, all'interno del processo). Se la logica di calcolo reale non è in grado di gestire una determinata combinazione di parametri di input e stati interni o ha una soluzione semplice senza eseguire l'algoritmo, non aiuta a far incapsulare tutto il codice (a volte in profondità) se i blocchi. Questo è uno "stato eccezionale", non parte della logica di base, quindi devo uscire dal calcolo non appena ho rilevato. In questo caso non esiste nessun altro ramo, in condizioni normali l'esecuzione continua semplicemente. (Naturalmente, "lo stato eccezionale" viene espresso meglio lanciando un'eccezione, ma a volte è eccessivo.)

Per esempio:

public bool myFunction()
{
   String myString = getString();

   if (null == myString)
   {
     // there is nothing to do if myString is null
     return false;
   } 

   myString = "Name " + myString;
   // Do something more here...

   // not using return value variable now, because the operation is straightforward.
   // if the operation is complex, use the variable as the previous example.

   return true;
}

La regola "un'uscita" aiuta anche quando il calcolo richiede risorse esterne che è necessario rilasciare o indica che è necessario reimpostare prima di uscire dalla funzione; a volte vengono aggiunti successivamente durante lo sviluppo. Con più uscite all'interno dell'algoritmo, è molto più difficile estendere correttamente tutti i rami. (E se possono verificarsi eccezioni, il rilascio / ripristino dovrebbe essere inserito anche in un blocco finalmente, per evitare effetti collaterali in rari casi eccezionali ...).

Il tuo caso sembra rientrare nella categoria "uscita rapida prima del lavoro reale" e lo scriverei come la tua versione dell'esempio 2.


9
+1 per "Non mi piacciono le funzioni con diversi punti di uscita"
Corv1nus,

@LorandKedves - ha aggiunto un esempio - spero che non ti dispiaccia.
Matthew Flynn,

@MatthewFlynn bene, se non ti dispiace che sia opposto a quello che ho suggerito alla fine della mia risposta ;-) Continuo l'esempio, spero che alla fine andrà bene per entrambi :-)
Lorand Kedves

@MatthewFlynn (scusate, sono solo un bastardo bastardo, e grazie per avermi aiutato a chiarire la mia opinione. Spero vi piaccia la versione attuale.)
Lorand Kedves,

13
Di funzioni con più punti di uscita e funzioni con una variabile di risultato mutevole, la prima mi sembra di gran lunga il minore dei due mali.
Jon Purdy,

9

Preferisco impiegare blocchi di guardia ogni volta che posso per due motivi:

  1. Consentono un'uscita rapida data una condizione specifica.
  2. Rimuove la necessità di istruzioni complesse e non necessarie se più avanti nel codice.

In generale, preferisco vedere i metodi in cui la funzionalità principale del metodo è chiara e minima. I blocchi di guardia aiutano visivamente a far sì che ciò accada.


5

Mi piace l'approccio "Fall Through":

public bool MyFunction()
{
   string myString = GetString();

   if (myString != null)
   {
     myString = "Name " + myString;
     return true;
    }
    return false;
}

L'azione ha una condizione specifica, qualsiasi altra cosa è solo il ritorno "fall through" predefinito.


1

Se avessi una sola condizione if non passerei troppo tempo a rimuginare sullo stile. Ma se ho più condizioni di guardia preferirei style2

Immagina questo. Supponi che i test siano complessi e non vuoi davvero legarli in una singola condizione if-ORed per evitare la complessità:

//Style1
if (this1 != Right)
{ 
    return;
}
else if(this2 != right2)
{
    return;
}
else if(this3 != right2)
{
    return;
}
else
{
    //everything is right
    //do something
    return;
}

contro

//Style 2
if (this1 != Right)
{ 
   return;
}
if(this2 != right2)
{
    return;
}
if(this3 != right2)
{
    return;
}


//everything is right
//do something
return;

Qui ci sono due vantaggi principali

  1. Stai separando il codice in una singola funzione in due blocchi visivamente logici: un blocco superiore di convalide (condizioni di protezione) e un blocco inferiore di codice eseguibile.

  2. Se devi aggiungere / rimuovere una condizione, riduci le possibilità di incasinare l'intera scala if-elseif-else.

Un altro vantaggio minore è che hai una serie di parentesi graffe di cui occuparti.


0

Questa dovrebbe essere una questione di cui suona meglio.

If condition
  do something
else
  do somethingelse

si esprime meglio allora

if condition
  do something
do somethingelse

per i metodi di piccole dimensioni non farà molta differenza, ma per i metodi composti più grandi può rendere più difficile la comprensione poiché non sarà adeguatamente separato


3
Se un metodo è così lungo che la seconda forma è difficile da capire, è troppo lunga.
Kevin Cline,

7
I tuoi due casi sono equivalenti solo se do somethinginclude un reso. In caso contrario, il secondo codice verrà eseguito do somethingelseindipendentemente dal ifpredicato che non è il modo in cui funziona il primo blocco.
Adnan,

Sono anche ciò che l'OP stava spiegando nei suoi esempi. Proprio seguendo la linea della domanda
José Valente,

0

Trovo irritante l'esempio 1, perché la dichiarazione di ritorno mancante alla fine di una funzione di restituzione del valore attiva immediatamente la bandiera "Aspetta, c'è qualcosa di sbagliato". Quindi in casi come questo, andrei con l'esempio 2.

Tuttavia, di solito c'è di più, a seconda dello scopo della funzione, ad esempio gestione degli errori, registrazione, ecc. Quindi sono con la risposta di Lorand Kedves su questo, e di solito ho un punto di uscita alla fine, anche a costo di una variabile flag aggiuntiva. Semplifica la manutenzione e le estensioni successive nella maggior parte dei casi.


0

Quando la tua ifdichiarazione ritorna sempre, non c'è motivo di usare un altro codice rimanente nella funzione. In questo modo si aggiungono righe extra e rientri extra. L'aggiunta di codice non necessario rende più difficile la lettura e, come tutti sanno, leggere il codice è difficile .


0

Nel tuo esempio, elseovviamente non è necessario, MA ...

Quando si scorre rapidamente attraverso le righe del codice, gli occhi in genere guardano le parentesi graffe e le rientranze per farsi un'idea del flusso del codice; prima che si depositino sul codice stesso. Pertanto, scrivere il else, e mettere parentesi graffe e rientri attorno al secondo blocco di codice rende in realtà più veloce per il lettore vedere che questa è una situazione "A o B", dove verrà eseguito un blocco o l'altro. Cioè, il lettore vede le parentesi graffe e il rientro PRIMA di vedere il returndopo l'iniziale if.

A mio avviso, questo è uno di quei rari casi in cui l'aggiunta di qualcosa di ridondante al codice lo rende effettivamente PIÙ leggibile, non meno.


0

Preferirei l'esempio 2 perché è immediatamente ovvio che la funzione restituisce qualcosa . Ma anche di più, preferirei tornare da un posto, in questo modo:

public bool MyFunction()
{
    bool result = false;

    string myString = GetString();

    if (myString != nil) {
        myString = "Name " + myString;

        result = true;
    }

    return result;
}

Con questo stile posso:

  1. Vedi subito che sto restituendo qualcosa.

  2. Cattura il risultato di ogni invocazione della funzione con un solo punto di interruzione.


Questo è simile a come approccerei il problema. L'unica differenza è che vorrei utilizzare la variabile risultato per acquisire la valutazione myString e utilizzarla come valore di ritorno.
Chuck Conway,

@ChuckConway Concordato, ma stavo cercando di attenermi al prototipo dell'OP.
Caleb,

0

Il mio stile personale tende ad andare

function doQuery(string) {
    if (!query(string)) {
        query("ABORT");
        return false;
    } // else
    if(!anotherquery(string) {
        query("ABORT");
        return false;
    } // else
    return true;
}

Utilizzare le elseistruzioni commentate per indicare il flusso del programma e mantenerlo leggibile, evitando però rientri massicci che possono facilmente raggiungere attraverso lo schermo se sono coinvolti molti passaggi.


1
Personalmente, spero di non dover mai lavorare con il tuo codice.
un CVn del

Se hai più controlli, questo metodo potrebbe richiedere un po 'di tempo. La restituzione di ciascuna clausola if è simile all'utilizzo dell'istruzione goto per saltare sezioni del codice.
Chuck Conway,

Se fosse una scelta tra questo e il codice con le elseclausole incluse, sceglierei quest'ultimo perché anche il compilatore li ignorerà efficacemente. Sebbene dipenderebbe dal else if non aumentare il rientro più dell'originale una volta, se così fosse sarei d'accordo con te. Ma la mia scelta sarebbe quella di abbandonare elsecompletamente se quello stile fosse permesso.
Mark Hurd,

0

vorrei scrivere

public bool SetUserId(int id)
{
   // Get user name from id
   string userName = GetNameById(id);

   if (userName != null)
   {
       // update local ID and userName
       _id = id;
       _userNameLabelText = "Name: " + userName;
   }

   return userName != null;
}

Preferisco questo stile perché è ovvio che vorrei tornare userName != null.


1
La domanda è: perché preferisci quello stile?
Caleb,

... Francamente, non sono sicuro del motivo per cui vuoi anche la stringa in questo caso, per non parlare del motivo per cui stai aggiungendo un test booleano aggiuntivo alla fine che non ti serve davvero per nessun motivo.
Shadur,

@Shadur Sono a conoscenza di test booleani extra e myStringrisultati inutilizzati . Copio solo la funzione da OP. Lo cambierò in qualcosa che sembra più vita reale. Il test booleano è banale e non dovrebbe essere un problema.
tia,

1
@Shadur Non stiamo ottimizzando qui, giusto? Il mio punto è rendere il mio codice facile da leggere e mantenere, e personalmente lo pagherei con il costo di un controllo null di riferimento.
tia

1
@tia Mi piace lo spirito del tuo codice. Invece di valutare userName due volte, catturerei la prima valutazione in una variabile e la restituirei.
Chuck Conway,

-1

La mia regola empirica è quella di fare un if-return senza un altro (principalmente per errori) o fare una catena if-else-if completa su tutte le possibilità.

In questo modo evito di provare a essere troppo fantasioso con il mio branching, poiché ogni volta che finisco per farlo codifico la logica dell'applicazione nei miei if, rendendoli più difficili da mantenere (ad esempio, se un enum OK o ERR e scrivo tutti i miei if approfittando del fatto che! OK <-> ERR diventa una seccatura nel culo aggiungere una terza opzione all'enum la prossima volta.)

Nel tuo caso, ad esempio, userei un semplice se, poiché "return if null" è sicuramente un modello comune e non è probabile che dovrai preoccuparti di una terza possibilità (oltre a null / non null) nel futuro.

Tuttavia, se il test fosse qualcosa che faceva un'ispezione più approfondita sui dati, invece, avrei sbagliato verso un if-else completo.


-1

Penso che ci siano molte insidie ​​nell'esempio 2, che in fondo alla strada potrebbero portare a codice non intenzionale. Il primo obiettivo qui è basato sulla logica che circonda la variabile 'myString'. Pertanto, per essere espliciti, tutti i test delle condizioni dovrebbero avvenire in un blocco di codice tenendo conto della logica nota e predefinita / sconosciuta .

E se in seguito il codice fosse stato introdotto involontariamente nell'esempio 2 che ha modificato in modo significativo l'uscita:

   if (myString == null)
   {
      return false;
   }

   //add some v1 update code here...
   myString = "And the winner is: ";
   //add some v2 update code here...
   //buried in v2 updates the following line was added
   myString = null;
   //add some v3 update code here...
   //Well technically this should not be hit because myString = null
   //but we already passed that logic
   myString = "Name " + myString;
   // Do something more here...
   return true;

Penso che con il elseblocco immediatamente successivo alla verifica di un valore null i programmatori che hanno aggiunto i miglioramenti alle versioni future aggiungano insieme tutta la logica perché ora abbiamo una stringa di logica non intenzionale per la regola originale (che restituisce se il valore è nullo).

Prendo fortemente la mia fiducia in questo in alcune delle linee guida C # su Codeplex (link a quello qui: http://csharpguidelines.codeplex.com/ ) che affermano quanto segue:

"Aggiungi un commento descrittivo se si suppone che il blocco predefinito (altro) sia vuoto. Inoltre, se non si ritiene che quel blocco debba essere raggiunto, lancia un InvalidOperationException per rilevare future modifiche che potrebbero cadere nei casi esistenti. Ciò garantisce un codice migliore, perché è stato pensato a tutti i percorsi che il codice può percorrere. "

Penso che sia una buona pratica di programmazione quando si utilizzano blocchi logici come questo avere sempre un blocco predefinito aggiunto (if-else, case: default) per tenere conto di tutti i percorsi di codice in modo esplicito e non lasciare il codice aperto a conseguenze logiche indesiderate.


In che modo non avere un altro esempio 2 non prende in considerazione tutti i casi? Il risultato previsto è l'esecuzione continua. L'aggiunta di una clausola else in questo caso aumenta solo la complessità del metodo.
Chuck Conway,

Non sono d'accordo sulla base della capacità di non avere tutte le logiche interessate in questione in un blocco conciso e deterministico. L'attenzione si concentra sulla manipolazione del myStringvalore e il codice successivo al ifblocco è la logica risultante se la stringa! = Null. Pertanto, poiché l'attenzione si concentra sulla logica aggiuntiva che manipola quel valore specifico , penso che dovrebbe essere incapsulato in un elseblocco. Altrimenti questo apre il potenziale mentre ho mostrato di separare involontariamente la logica e introdurre conseguenze indesiderate. Potrebbe non succedere tutto il tempo, ma questa è stata davvero una domanda sullo stile di opinione delle migliori pratiche .
atconway
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.