Chiarimento del consiglio "avoid if-else" [duplicato]

31

Gli esperti di codice pulito consigliano di non utilizzare if/else poiché sta creando un codice illeggibile. Suggeriscono piuttosto di usare IF e di non aspettare fino alla fine di un metodo senza reale necessità.

Ora, questo consiglio di if/else mi confonde. Dicono che non dovrei usare if/else affatto (!) o evitare if/else nidificazione?

Inoltre, se si riferiscono al nidificazione di if/else , non dovrei fare nemmeno un singolo nidificazione o dovrei limitarlo a un massimo di 2 nidificazioni (come alcuni raccomandano)?

Quando dico nidificazione singola, intendo questo:

    if (...) {

        if (...) {

        } else {

        }

    } else {

    }

Modifica

Anche strumenti come Resharper suggeriranno di riformattare le dichiarazioni if/else . Di solito li esibisce in una dichiarazione stand-alone if e talvolta persino in un'espressione ternaria.

    
posta deviDave 01.08.2013 - 17:04
fonte

6 risposte

37

Penso che questo consiglio provenga da una metrica software che ha chiamato Complessità ciclomatica o Complessità condizionale verifica questa pagina wiki

Definizione:

The cyclomatic complexity of a section of source code is the count of the number of linearly independent paths through the source code. For instance, if the source code contained no decision points such as IF statements or FOR loops, the complexity would be 1, since there is only a single path through the code. If the code had a single IF statement containing a single condition there would be two paths through the code, one path where the IF statement is evaluated as TRUE and one path where the IF statement is evaluated as FALSE.

Do they say that I should not use if/else at all (!) or to avoid if/else nesting?

No, non dovresti evitare se / else! ma dovresti fare attenzione alla complessità ciclomatica del tuo codice. Più complessità del codice tende ad avere più difetti. Secondo questo il limite corretto sarebbe 11 . Uno strumento chiamato Codice completo categorizza il punteggio in questo modo Ref :.

  • 0-5 : probabilmente la routine è soddisfacente
  • 6-10 : inizia a pensare ai modi per semplificare la routine
  • 10 + - interrompi parte della routine in una seconda routine e chiamala dalla prima routine

Come calcolare la complessità ciclomatica "in pratica":

Dall'esempio qui : considera il seguente pseudo codice:

if (A=354) {

    if (B>C) {/*decision 1*/ A=B} else {/*decision 2*/ A=C}

   }/*decision 3 , in case A is NOT equal 354*/ 
print A

tieni presente che questa metrica è preoccupata per il numero di decisioni nel tuo programma / codice, quindi dal codice precedente potremmo dire che abbiamo 3 decisioni (3 valori di A)

calcoliamolo formalmente quindi considera il seguente flusso di controllo del codice:

LacomplessitàMvienequindidefinita*come: Rif

M = E − N + 2P

dove

E = the number of edges of the graph
N = the number of nodes of the graph
P = the number of connected components (exit nodes).

applicando la formula E(flow lines) = 8 , N (nodes)= 7 , P (exit nodes) =1 then M = 8-7 + (2*1) = 3

* c'è anche un'altra definizione dichiarata ma è vicina: M = E − N + P controlla il riferimento per la teoria.

Tieni presente che molti degli strumenti di analisi del codice statico potrebbero calcolare la complessità ciclomatica del tuo codice.

    
risposta data 01.08.2013 - 17:13
fonte
53

IMO, le risposte precedenti si concentrano su casi limite e codice che dovrebbero essere comunque riscritti. Ecco alcuni casi che vedo quasi ovunque in cui if/else avrebbe dovuto essere sostituito da if o da un altro costrutto linguistico.

Nota: creo la wiki della comunità di risposta. Sentiti libero di modificarlo e aggiungi altri esempi di casi in cui if/else di solito porta a codice errato.

Principianti, sii avvisato ! if/else ha il potenziale di creare un codice illeggibile se non usato con attenzione. Gli esempi seguenti mostrano i casi più comuni in cui if/else è facilmente utilizzato in modo improprio.

1. Clausola di protezione

Ho smesso di contare quante volte ho visto qualcosa di simile:

void Demo(...)
{
    if (ok_to_continue)
    {
        ... // 20 lines here.
    }
    else
    {
        throw new SomeException();
    }
}

Nessuno ha bisogno di ulteriori indentazioni. Lo stesso codice avrebbe potuto essere scritto in questo modo:

void Demo(...)
{
    if (!ok_to_continue)
    {
        throw new SomeException();
    }

    ... // No indentation for the next 20 lines.
}

Regola: se puoi rimuovere un'istruzione else invertendo il controllo if , gestendo un errore ed uscendo immediatamente (altrimenti prosegui altrimenti), esegui tale operazione.

2. Duplicazione del codice

Anche questo è abbastanza frequente.

if (!this.useAlternatePanel)
{
    currentInput = this.defaultTextBox.Text;
    bool missingInput = currentInput.Length == 0;
    this.defaultProcessButton.Enabled = !missingInput;
    this.defaultMissingInputHelpLabel.Visible = missingInput;
}
else
{
    currentInput = this.alternateTextBox.Text;
    bool missingInput = currentInput.Length == 0;
    this.alternateProcessButton.Enabled = !missingInput;
    this.alternateMissingInputHelpLabel.Visible = missingInput;
}

Questo è un eccellente esempio di codice per principianti, perché un principiante non vedrebbe sempre come tale codice può essere rifatto. Uno dei modi è creare una mappa di controlli:

var defaultControls = new CountriesProcessingControls(
    this.defaultTextBox,
    this.defaultProcessButton,
    this.defaultMissingInputHelpLabel);

var alternateControls = new CountriesProcessingControls(
    this.alternateTextBox,
    this.alternateProcessButton,
    this.alternateMissingInputHelpLabel);

void RefreshCountriesControls(CountriesProcessingControls controls)
{
    currentInput = controls.textBox.Text;
    bool missingInput = currentInput.Length == 0;
    controls.processButton.Enabled = !missingInput;
    controls.missingInputHelpLabel.Visible = missingInput;
}

RefreshCountriesControls(this.useAlternatePanel ? alternateControls : defaultControls);

Regola: se il blocco in else è quasi identico al blocco in if , stai sbagliando e causando la duplicazione del codice.

3. Cercando di fare due cose

È anche vero il contrario:

if (...)
{
    foreach (var dateInput = this.selectedDates)
    {
        var isConvertible = convertToDate(dateInput);
        if (!isConvertible)
        {
            this.invalidDates.add(dateInput);
        }
    }
}
else
{
    var languageService = this.initializeLanguageService();
    this.Translated = languageService.translate(this.LastInput);
    if (this.Translated != null)
    {
        this.NotificationScheduler.append(LAST_INPUT_TRANSLATED);
    }
}

Il blocco in if non ha nulla a che fare con il blocco in else . La combinazione di due blocchi nello stesso metodo rende la logica difficile da comprendere e soggetta a errori. Trovare un nome per il metodo e anche commentarlo correttamente è estremamente difficile.

Regola: evita di combinare blocchi che non hanno nulla in comune insieme in un blocco if/else .

4. Programmazione orientata agli oggetti

Questo tipo di codifica è frequente anche tra i principianti:

// 'animal' is either a dog or a cat.
if (animal.IsDog)
{
    animal.EatPedigree();
    animal.Bark();
}
else // The animal is a cat.
{
    animal.EatWhiskas();
    animal.Meow();
}

Nella programmazione orientata agli oggetti, questo codice non dovrebbe esistere. Dovrebbe essere scritto in questo modo:

IAnimal animal = ...;
animal.Eat();
animal.MakeSound();

dato che ogni classe ( Cat : IAnimal e Dog : IAnimal ) avrà la propria implementazione di quei metodi.

Regola: quando si esegue la programmazione orientata agli oggetti, si generalizzano le funzioni (metodi) e si utilizzano l'ereditarietà e il polimorfismo.

5. Assegnazione di un valore

Vedo questo ovunque nelle codebase scritte dai principianti:

int price;
if (this.isDiscount)
{
    price = -discount;
}
else
{
    price = unitCost * quantity;
}

Questo è un codice terribile, dal momento che non è scritto per gli umani.

  • È fuorviante. Ti fa pensare che l'operazione principale qui è la scelta tra la modalità sconto e quella non scontata, mentre l'operazione principale effettiva è l'assegnazione di price .

  • È soggetto a errori. Cosa succede se in seguito, una condizione è cambiata? Cosa succede se uno dei due compiti viene rimosso?

  • Invita codice ancora peggiore. Ho visto un sacco di pezzi come quello in cui int price era invece int price = 0; , giusto per sbarazzarsi dell'avvertimento del compilatore se manca uno dei compiti. Nascondere gli avvertimenti invece di risolverli è una delle peggiori cose che si possa fare.

  • C'è una minuscola duplicazione del codice, che significa più caratteri da digitare in origine e più modifiche da fare in seguito.

  • È inutilmente lungo.

Lo stesso potrebbe essere scritto in questo modo:

int price = this.isDiscount ? -discount : unitCost * quantity;

Regola: usa l'operatore ternario invece dei blocchi if/else per assegnazioni di valore semplici.

    
risposta data 30.10.2018 - 00:28
fonte
7

Mi piace molto quello che Abdurahman ha detto nella sua risposta.

Un altro modo di considerare la frase "avoid if / else" è pensare in termini di decisioni.

L'obiettivo è evitare confusione quando si segue il codice. Quando si hanno più istruzioni if / else che sono annidate, diventa difficile comprendere l'obiettivo principale delle funzioni. Il programmatore che scrive il codice comprende la logica ora . MA, quando lo stesso programmatore rivisita quel codice un anno o due lungo la strada, la comprensione è generalmente dimenticata. Un nuovo programmatore che osserva il codice deve essere in grado di identificare cosa sta succedendo. Quindi l'obiettivo è di rompere la complessità in funzioni semplici e facili da capire. L'obiettivo non è evitare le decisioni, ma stabilire un flusso logico di facile comprensione.

    
risposta data 01.08.2013 - 18:23
fonte
7

Il nesting dei blocchi di codice, noto come anti-pattern a "punta di freccia", riduce la leggibilità del codice perché devi continuare a spostarti a destra, riducendo la larghezza disponibile per le linee di codice "reale" sullo schermo, oppure devi violare il indentazione e altre regole di formattazione che nel caso normale rendono più facile leggere il codice.

Caso in questione:

if (X) {
    if (Y) {
       DoXandY();
    } else {
       DoXOnly();
    }
} else {
   DoDefault();
}

Questo è un semplice esempio e non avrei problemi con alcuni livelli di questo tipo di nidificazione, fornito che il totale LOC per questa struttura era solo su uno schermo lungo e che il la formattazione coinvolta non aggiungeva troppo spazio bianco sul lato sinistro (si consideri l'utilizzo di 3 o 4 spazi invece di uno spazio tabspace, specialmente se il codice verrà visualizzato in altri editor in cui uno spazio tabs può essere di 8 spazi o più). Oltre a questo, potresti dimenticare o non essere in grado di vedere facilmente la condizione iniziale in cui gli altri sono annidati. Questo può farti "perdere" nella base di codice, specialmente se hai una struttura dove X e Y sono indipendenti:

if (X) {
    if (Y) {
       DoXandY();
    } else {
       DoXOnly();
    }
} else {
   if (Y) {
       DoYOnly();
    } else {
       DoDefault();
    }
}

In questo universo alternativo alla domanda specifica dell'OP, ora abbiamo due controlli di condizione "if (Y)" e se, in un vero pezzo di codice, non potremmo vedere la condizione genitore annidata, potremmo pensare che Ci troviamo in un posto diverso nel codice rispetto a quello che realmente siamo, e questo causa bug.

Un modo per ridurre il nidificazione è di distribuire la condizione esterna nelle condizioni interne. Tornando allo snippet iniziale (no "DoYOnly ()"), considera quanto segue:

if (X && Y) {
    DoXandY();
} else if (X) {
    DoXOnly();
} else {
    DoDefault();
}

Guarda Ma, non annidare! Al costo di una valutazione aggiuntiva di X (che, se è computazionalmente complessa per valutare X, è possibile eseguire front e memorizzare in una variabile booleana), si rende la struttura un albero di decisione if / else a livello singolo e quando modificando ogni ramo, si conosce la condizione che deve essere vera per inserirlo. Devi ancora ricordare che non era vero per arrivare a quel "else if", ma poiché tutto è allo stesso livello dovrebbe essere più facile identificare i rami precedenti dell'albero decisionale. In alternativa, puoi rimuovere tutte le altre parole chiave testando una versione più completa della condizione ad ogni passaggio:

if (X && Y) {
    DoXandY();
} 
if (X && !Y) {
    DoXOnly();
} 
if(!X) {
    DoDefault();
}

Ora sai esattamente cosa deve essere vero per ogni istruzione da eseguire. Tuttavia, questo inizia a trovare una linea di altri problemi di leggibilità; diventa più difficile vedere che tutte queste affermazioni si escludono a vicenda (che fa parte di ciò che un blocco "else" fa per te).

Altri modi per includere il raggruppamento includono l'inversione di una o più condizioni. Se la maggior parte o tutto il codice di una funzione si trova all'interno di un'istruzione if che verifica una condizione, in genere è possibile salvare il livello di nidificazione verificando invece che la condizione sia non true e eseguendo qualsiasi cosa 'fare in un altro blocco oltre l'originale se istruzione, prima di tornare fuori dalla funzione. Ad esempio:

if(!X) {
    DoDefault();
    return;
}

if(Y) {
    DoXAndY();
} else {
    DoXOnly();
}

Si comporta allo stesso modo di entrambe le affermazioni precedenti, ma verifica solo una volta ciascuna condizione e non ha if annidati. Questo schema è talvolta noto come "clausola di salvaguardia"; il codice sotto l'istruzione if iniziale non verrà mai eseguito se X è falso, quindi puoi dire che l'istruzione if "custodisce" il resto del codice (che è il motivo per cui non abbiamo mai bisogno di verificare che X sia vero nel resto del codice; se arriviamo a quella linea, X è vero perché altrimenti saremmo usciti presto dalla funzione). Lo svantaggio è che questo non funziona bene per il codice che è una parte più piccola di una funzione più grande. Se esistesse una funzione "DoMoreWork ()" dopo l'intero snippet precedente, dovrebbe essere inclusa sia nel codice della clausola di guardia che al di sotto dell'istruzione condizionale rimanente. Oppure, la clausola di guardia potrebbe essere integrata con il codice rimanente usando il modello "else if":

if(!X) {
    DoDefault();        
} else if(Y) {
    DoXAndY();
} else {
    DoXOnly();
}

DoMoreWork();

In questo esempio, DoMoreWork () viene sempre eseguito anche se X è falso. Aggiungere la stessa chiamata sotto lo snippet precedente con l'istruzione return causerebbe la chiamata di DoMoreWork solo se X è vero, ma indipendentemente dal fatto che Y sia vero o falso.

    
risposta data 01.08.2013 - 19:44
fonte
1

Oltre alle altre risposte, il consiglio può anche significare che if/else dovrebbe essere sostituito con if/else if dove appropriato. Quest'ultimo è più informativo e leggibile (esclusi ovviamente casi banali, come if (x > 0) else if (x <= 0) ).

if/else if ha un vantaggio in più del semplice essere più leggibile (anche se questo è un vantaggio considerevole). Aiuta il programmatore a evitare errori nel caso in cui ci siano più di due rami a seconda delle condizioni. Questo stile renderà il caso missed più ovvio.

    
risposta data 01.08.2013 - 18:48
fonte
1

Il motivo per cui ti consiglia di utilizzare solo if e non else è di evitare il classico bug di if this then for everything else do this . L'ambito di else è difficile da prevedere.

Ad esempio; diciamo che sto programmando i controlli per una macchina che guida da solo.

if (light is red) then {
    stop car
} else {
    drive forward
}

Mentre la logica è sana. Quando la macchina arriva a una luce gialla continuerà a guidare attraverso l'incrocio. Probabilmente causa un incidente quando avrebbe dovuto rallentare e fermarsi.

Senza if/else la logica è meno incline agli errori.

if(light is red) then {
  stop car
}
if(light is yellow) then {
  slow down
}
if(light is green) then {
  drive forward
}

Mentre il mio codice potrebbe non essere la migliore auto a guida autonoma. Dimostra solo la differenza tra una condizione ambigua e condizioni esplicite. Il che è probabilmente anche l'autore. Non utilizzare else per la logica di scelta rapida.

    
risposta data 01.08.2013 - 19:16
fonte

Leggi altre domande sui tag