Invertire l'istruzione "if" per ridurre l'annidamento


272

Quando ho eseguito ReSharper sul mio codice, ad esempio:

    if (some condition)
    {
        Some code...            
    }

ReSharper mi ha dato l'avvertimento di cui sopra (Inverti istruzione "if" per ridurre l'annidamento) e mi ha suggerito la seguente correzione:

   if (!some condition) return;
   Some code...

Vorrei capire perché è meglio. Ho sempre pensato che usare "return" nel mezzo di un metodo problematico, un po 'come "goto".


1
Credo che l'eccezione che controlli e ritorni all'inizio vada bene, ma cambierei la condizione in modo che tu stia verificando l'eccezione direttamente piuttosto che non qualcosa (es. Se (qualche condizione) ritorna).
Bruceatk,

36
No, non farà nulla per le prestazioni.
Seth Carnegie,

3
Sarei tentato di lanciare ArgumentException se al mio metodo venissero trasmessi dati non validi.
asawyer,

1
@asawyer Sì, qui c'è un'intera discussione laterale sulle funzioni che perdonano troppo gli input senza senso, invece di usare un errore di asserzione. Scrivere Solid Code mi ha aperto gli occhi su questo. Nel qual caso, questo sarebbe qualcosa di simile ASSERT( exampleParam > 0 ).
Greg Hendershott l'

4
Le asserzioni riguardano lo stato interno, non i parametri. Inizieresti convalidando i parametri, affermando che il tuo stato interno è corretto e quindi eseguendo l'operazione. In una build di rilascio, è possibile tralasciare le asserzioni o mapparle su un arresto del componente.
Simon Richter,

Risposte:


296

Un ritorno nel mezzo del metodo non è necessariamente negativo. Potrebbe essere meglio restituire immediatamente se rende più chiaro l'intento del codice. Per esempio:

double getPayAmount() {
    double result;
    if (_isDead) result = deadAmount();
    else {
        if (_isSeparated) result = separatedAmount();
        else {
            if (_isRetired) result = retiredAmount();
            else result = normalPayAmount();
        };
    }
     return result;
};

In questo caso, se _isDeadè vero, possiamo immediatamente uscire dal metodo. Potrebbe essere meglio strutturarlo in questo modo invece:

double getPayAmount() {
    if (_isDead)      return deadAmount();
    if (_isSeparated) return separatedAmount();
    if (_isRetired)   return retiredAmount();

    return normalPayAmount();
};   

Ho scelto questo codice dal catalogo di refactoring . Questo refactoring specifico è chiamato: Sostituisci condizionale nidificato con clausole di guardia.


13
Questo è davvero un bell'esempio! Il codice refactored è più simile a un'istruzione case.
Altri

16
Probabilmente solo una questione di gusti: suggerisco di cambiare il 2 ° e il 3 ° "if" in "else if" per aumentare ulteriormente la leggibilità. Se si trascura l'istruzione "return", sarebbe comunque chiaro che il caso seguente viene verificato solo se il precedente non è riuscito, vale a dire che l'ordine dei controlli è importante.
foraidt

2
In un esempio, questo semplice id concorda che la seconda via è migliore, ma solo perché è ovvio ciò che sta accadendo.
Andrew Bullock,

Ora, come possiamo prendere quel bel codice jop scritto e impedire a Visual Studio di scomporlo e mettere i rendimenti su righe separate? Mi irrita davvero che riformatta il codice in quel modo. Rende UGLY questo codice molto leggibile.
Segna il

@Mark T Ci sono impostazioni in Visual Studio per impedire che si interrompa quel codice.
Aaron Smith,

333

Non è solo estetico , ma riduce anche il massimo livello di annidamento all'interno del metodo. Questo è generalmente considerato un vantaggio perché semplifica la comprensione dei metodi (e in effetti, molti strumenti di analisi statica forniscono una misura di questo come uno degli indicatori della qualità del codice).

D'altra parte, fa anche in modo che il tuo metodo abbia più punti di uscita, qualcosa che un altro gruppo di persone crede sia un no-no.

Personalmente, sono d'accordo con ReSharper e il primo gruppo (in una lingua che ha delle eccezioni trovo sciocco discutere di "punti di uscita multipli"; quasi tutto può essere lanciato, quindi ci sono numerosi potenziali punti di uscita in tutti i metodi).

Per quanto riguarda le prestazioni : entrambe le versioni dovrebbero essere equivalenti (se non a livello di IL, quindi sicuramente dopo il jitter con il codice) in ogni lingua. Teoricamente questo dipende dal compilatore, ma praticamente qualsiasi compilatore oggi ampiamente utilizzato è in grado di gestire casi di ottimizzazione del codice molto più avanzati di così.


41
punti di uscita singoli? Chi ne ha bisogno?
sq33G

3
@ sq33G: Quella domanda su SESE (e le risposte ovviamente) sono fantastiche. Grazie per il link!
Jon

Continuo a sentire nelle risposte che ci sono persone che sostengono singoli punti di uscita, ma non ho mai visto qualcuno che lo sostenesse, specialmente in lingue come C #.
Thomas Bonini,

1
@AndreasBonini: l'assenza di prova non è prova di assenza. :-)
Jon

Sì, certo, trovo strano che tutti sentano il bisogno di dire che ad alcune persone piace un altro approccio se quelle persone non sentono il bisogno di dirlo da soli =)
Thomas Bonini,

102

Questo è un po 'un argomento religioso, ma concordo con ReSharper che dovresti preferire meno nidificazione. Credo che questo superi i negativi di avere più percorsi di ritorno da una funzione.

Il motivo principale per avere meno annidamenti è migliorare la leggibilità e la manutenibilità del codice . Ricorda che molti altri sviluppatori dovranno leggere il tuo codice in futuro e il codice con meno rientri è generalmente molto più facile da leggere.

Le precondizioni sono un ottimo esempio di dove è possibile tornare presto all'inizio della funzione. Perché la leggibilità del resto della funzione dovrebbe essere influenzata dalla presenza di un controllo preliminare?

Per quanto riguarda gli aspetti negativi sul ritorno più volte da un metodo, i debugger sono piuttosto potenti ora, ed è molto facile scoprire esattamente dove e quando sta tornando una particolare funzione.

Avere più ritorni in una funzione non influirà sul lavoro del programmatore di manutenzione.

La scarsa leggibilità del codice lo farà.


2
I ritorni multipli in una funzione hanno effetto sul programmatore di manutenzione. Durante il debug di una funzione, cercando il valore restituito non valido, il manutentore deve inserire punti di interruzione in tutti i punti che possono restituire.
EvilTeach

10
Metterei un punto di interruzione sul tutore di apertura. Se si passa attraverso la funzione, non solo è possibile vedere i controlli eseguiti in ordine, ma sarà chiaramente chiaro su quale controllo la funzione è atterrata per quella corsa.
John Dunagan,

3
Concordo con John qui ... Non vedo il problema nel solo passare attraverso l'intera funzione.
Nailer,

5
@Nailer Molto tardi, sono particolarmente d'accordo, perché se la funzione è troppo grande per essere passata, dovrebbe essere comunque suddivisa in più funzioni!
Aidiakapi,

1
Concordo anche con John. Forse è una vecchia scuola pensata per mettere il punto di rottura in fondo, ma se sei curioso di sapere quale funzione sta tornando, passerai attraverso detta funzione per vedere perché sta restituendo ciò che sta comunque ritornando. Se vuoi solo vedere cosa ritorna, mettilo nell'ultima parentesi.
user441521

70

Come altri hanno già detto, non ci dovrebbe essere un successo nelle prestazioni, ma ci sono altre considerazioni. A parte queste valide preoccupazioni, questo può anche aprirti ai gotchas in alcune circostanze. Supponiamo che tu abbia a che fare con un doubleinvece:

public void myfunction(double exampleParam){
    if(exampleParam > 0){
        //Body will *not* be executed if Double.IsNan(exampleParam)
    }
}

In contrasto con l' inversione apparentemente equivalente:

public void myfunction(double exampleParam){
    if(exampleParam <= 0)
        return;
    //Body *will* be executed if Double.IsNan(exampleParam)
}

Quindi in certe circostanze ciò che sembra essere invertito correttamente ifpotrebbe non esserlo.


4
Va anche notato che la correzione rapida del resharper inverte esempioParam> 0 in esempioParam <0 non esempioParam <= 0 che mi ha sorpreso.
Nick

1
Ed è per questo che quel controllo dovrebbe essere anticipato e restituito anche dato che lo sviluppatore pensa che un nan dovrebbe comportare il salvataggio.
user441521

51

L'idea di tornare solo alla fine di una funzione è tornata dai giorni prima che le lingue avessero il supporto per le eccezioni. Ha permesso ai programmi di fare affidamento sul fatto di poter mettere il codice di pulizia alla fine di un metodo, e quindi essere sicuro che sarebbe stato chiamato e qualche altro programmatore non avrebbe nascosto un ritorno nel metodo che ha causato il salto del codice di pulizia . Il codice di pulizia ignorato potrebbe provocare una perdita di memoria o di risorse.

Tuttavia, in una lingua che supporta le eccezioni, non fornisce tali garanzie. In un linguaggio che supporta le eccezioni, l'esecuzione di qualsiasi istruzione o espressione può causare un flusso di controllo che provoca la fine del metodo. Ciò significa che la pulizia deve essere eseguita utilizzando il metodo finally o utilizzando le parole chiave.

Ad ogni modo, sto dicendo che penso che molte persone citino le linee guida sul "solo ritorno alla fine di un metodo" senza capire perché sia ​​mai stata una buona cosa da fare e che ridurre la nidificazione per migliorare la leggibilità sia probabilmente un obiettivo migliore.


6
Hai appena capito il motivo per cui le eccezioni sono UglyAndEvil [tm] ... ;-) Le eccezioni sono goto sotto mentite spoglie, costose.
EricSchaefer,

16
@Eric devi essere stato esposto a codice eccezionalmente negativo. È abbastanza ovvio quando vengono utilizzati in modo errato e in genere consentono di scrivere codice di qualità superiore.
Robert Paulson,

Questa è la risposta che stavo davvero cercando! Ci sono ottime risposte qui, ma la maggior parte di esse si concentra su esempi, non sul vero motivo per cui è nata questa raccomandazione.
Gustavo Mori,

Questa è la risposta migliore, poiché spiega perché tutto questo argomento è nato in primo luogo.
Buttle Butkus,

If you've got deep nesting, maybe your function is trying to do too many things.=> questa non è una conseguenza corretta della tua frase precedente. Perché poco prima di dire che puoi modificare il comportamento A con il codice C nel comportamento A con il codice D. il codice D è più pulito, garantito, ma "troppe cose" si riferiscono al comportamento, che NON è cambiato. Pertanto non ha alcun senso questa conclusione.
v.oddou,

30

Vorrei aggiungere che esiste un nome per quegli if rovesciati: Guard Clause. Lo uso ogni volta che posso.

Odio leggere il codice dove c'è se all'inizio, due schermate di codice e nessun altro. Basta invertire se e tornare. In questo modo nessuno perderà tempo a scorrere.

http://c2.com/cgi/wiki?GuardClause


2
Esattamente. È più di un refactoring. Aiuta a leggere il codice più facilmente come hai già detto.
rpattabi,

'ritorno' non è abbastanza e orribile per una clausola di guardia. Vorrei fare: se (ex <= 0) genera WrongParamValueEx ("[MethodName] valore parametro 1 input errato {0} ... e dovrai catturare l'eccezione e scrivere nel registro dell'app
JohnJohnGa

3
@John. Hai ragione se questo è in realtà un errore. Ma spesso non lo è. E invece di controllare qualcosa in ogni posto in cui il metodo è chiamato metodo, lo controlla e non fa nulla.
Piotr Perak,

3
Odierei leggere un metodo che consiste in due schermate di codice, punto, punto ifo no. lol
jpmc26

1
Personalmente preferisco i ritorni anticipati proprio per questo motivo (anche: evitare l'annidamento). Tuttavia, questo non è correlato alla domanda originale sulle prestazioni e dovrebbe probabilmente essere un commento.
Patrick M,

22

Non influisce solo sull'estetica, ma impedisce anche l'annidamento del codice.

Può effettivamente funzionare come condizione preliminare per garantire che anche i tuoi dati siano validi.


18

Questo è ovviamente soggettivo, ma penso che migliora notevolmente su due punti:

  • Ora è immediatamente ovvio che la tua funzione non ha più nulla da fare se è conditionvalida.
  • Mantiene basso il livello di nidificazione. La nidificazione danneggia la leggibilità più di quanto si pensi.

15

Più punti di restituzione erano un problema in C (e in misura minore C ++) perché ti costringevano a duplicare il codice di pulizia prima di ciascuno dei punti di restituzione. Con la raccolta dei rifiuti, il try| finallycostruzione e usingblocchi, non c'è davvero alcun motivo per cui dovresti aver paura di loro.

In definitiva, dipende da ciò che tu e i tuoi colleghi trovate più facile da leggere.


Questa non è l'unica ragione. C'è una ragione accademica che si riferisce allo pseudo codice che non ha nulla a che fare con considerazioni pratiche come la pulizia delle cose. Questa ragione acamedica ha a che fare con il rispetto delle forme base dei costrutti imperativi. Allo stesso modo in cui non si posizionano gli scaricatori di loop al centro. In questo modo, gli invarianti possono essere rigorosamente rilevati e il comportamento può essere dimostrato. O la risoluzione può essere dimostrata.
v.oddou,

1
notizie: in realtà ho trovato un motivo molto pratico per cui l'interruzione anticipata e i ritorni sono cattivi, ed è una conseguenza diretta di ciò che ho detto, l'analisi statica. ecco a voi, documento di guida all'uso del compilatore C ++ Intel: d3f8ykwhia686p.cloudfront.net/1live/intel/… . Frase chiave:a loop that is not vectorizable due to a second data-dependent exit
v.oddou,

12

Per quanto riguarda le prestazioni, non vi sarà alcuna differenza evidente tra i due approcci.

Ma la codifica non riguarda solo le prestazioni. Anche la chiarezza e la manutenibilità sono molto importanti. E, in questi casi in cui ciò non influisce sulle prestazioni, è l'unica cosa che conta.

Esistono scuole di pensiero in competizione su quale approccio sia preferibile.

Una vista è quella menzionata da altri: il secondo approccio riduce il livello di annidamento, migliorando la chiarezza del codice. Questo è naturale in uno stile imperativo: quando non hai più nulla da fare, potresti anche tornare presto.

Un altro punto di vista, dal punto di vista di uno stile più funzionale, è che un metodo dovrebbe avere un solo punto di uscita. Tutto in un linguaggio funzionale è un'espressione. Quindi, se le dichiarazioni devono sempre avere clausole else. Altrimenti l'espressione if non avrebbe sempre un valore. Quindi, nello stile funzionale, il primo approccio è più naturale.


11

Le clausole di protezione o le pre-condizioni (come probabilmente vedrai) controllano se una determinata condizione è soddisfatta e quindi interrompono il flusso del programma. Sono perfetti per i luoghi in cui sei davvero interessato solo a un risultato di una ifdichiarazione. Quindi, piuttosto che dire:

if (something) {
    // a lot of indented code
}

Si inverte la condizione e si rompe se tale condizione invertita è soddisfatta

if (!something) return false; // or another value to show your other code the function did not execute

// all the code from before, save a lot of tabs

return non è affatto sporco come goto . Ti consente di passare un valore per mostrare al resto del codice che la funzione non può essere eseguita.

Vedrai i migliori esempi di dove questo può essere applicato in condizioni nidificate:

if (something) {
    do-something();
    if (something-else) {
        do-another-thing();
    } else {
        do-something-else();
    }
}

vs:

if (!something) return;
do-something();

if (!something-else) return do-something-else();
do-another-thing();

Troverai poche persone che sostengono che il primo è più pulito, ma ovviamente è completamente soggettivo. Ad alcuni programmatori piace sapere in quali condizioni sta operando qualcosa per rientro, mentre preferirei mantenere il flusso del metodo lineare.

Non suggerirò per un momento che i precon ti cambieranno la vita o ti faranno scopare, ma potresti trovare il tuo codice un po 'più facile da leggere.


6
Immagino di essere uno dei pochi allora. Trovo più facile leggere la prima versione. Gli if nidificati rendono l'albero delle decisioni più ovvio. D'altra parte, se ci sono un paio di pre-condizioni, sono d'accordo che è meglio metterle tutte in cima alla funzione.
Altrimenti

@Altro: completamente d'accordo. i ritorni scritti in modo seriale fanno sì che il tuo cervello abbia bisogno di serializzare possibili percorsi. Quando l'if-albero può essere mappato direttamente in una logica transicente, ad esempio, potrebbe essere compilato in lisp. ma la moda del ritorno seriale richiederebbe un compilatore molto più difficile da fare. Il punto qui ovviamente non ha nulla a che fare con questo scenario ipotetico, ha a che fare con l'analisi del codice, le possibilità di ottimizzazione, le prove di correzione, le prove di terminazione e il rilevamento degli invarianti.
v.oddou,

1
Nessuno trova più facile leggere il codice con più nidi. Più blocco è simile a qualcosa, più è facile da leggere a colpo d'occhio. Non c'è modo in cui il primo esempio qui sia più facile da leggere e va solo 2 nidi quando la maggior parte del codice come questo nel mondo reale va più in profondità e con ogni nido richiede più potenza del cervello per seguire.
user441521

1
Se l'annidamento fosse due volte più profondo, quanto tempo impiegheresti a rispondere alla domanda: "Quando fai" qualcos'altro "?" Penso che sarebbe difficile trovare una risposta senza carta e penna. Ma potresti rispondere abbastanza facilmente se fosse tutto con clausole di guardia.
David Storfer,

9

Qui ci sono molti buoni punti, ma anche più punti di ritorno possono essere illeggibili , se il metodo è molto lungo. Detto questo, se hai intenzione di utilizzare più punti di ritorno assicurati solo che il tuo metodo sia breve, altrimenti il ​​bonus di leggibilità di più punti di ritorno potrebbe andare perso.


8

Le prestazioni sono in due parti. Hai prestazioni quando il software è in produzione, ma vuoi anche avere prestazioni durante lo sviluppo e il debug. L'ultima cosa che uno sviluppatore vuole è "aspettare" qualcosa di banale. Alla fine, compilando questo con l'ottimizzazione abilitata si otterrà un codice simile. Quindi è bello conoscere questi piccoli trucchi che ripagano in entrambi gli scenari.

Il caso nella domanda è chiaro, ReSharper è corretto. Invece di annidare le ifistruzioni e creare un nuovo ambito nel codice, stai impostando una regola chiara all'inizio del metodo. Aumenta la leggibilità, sarà più facile da mantenere e ridurrà la quantità di regole che si dovranno vagliare per trovare dove vogliono andare.


7

Personalmente preferisco solo 1 punto di uscita. È facile da realizzare se mantieni i tuoi metodi brevi e mirati e fornisce un modello prevedibile per la persona successiva che lavora sul tuo codice.

per esempio.

 bool PerformDefaultOperation()
 {
      bool succeeded = false;

      DataStructure defaultParameters;
      if ((defaultParameters = this.GetApplicationDefaults()) != null)
      {
           succeeded = this.DoSomething(defaultParameters);
      }

      return succeeded;
 }

Questo è anche molto utile se vuoi solo controllare i valori di alcune variabili locali all'interno di una funzione prima che esca. Tutto quello che devi fare è posizionare un breakpoint sul ritorno finale e sei sicuro di colpirlo (a meno che non venga generata un'eccezione).


4
bool PerformDefaultOperation () {DataStructure defaultParameters = this.GetApplicationDefaults (); ! ritorno (defaultParameters = NULL && this.DoSomething (defaultParameters);} Ci, fissati per voi :).
tchen

1
Questo esempio è molto semplice e manca il punto di questo schema. Avere circa 4 altri controlli all'interno di questa funzione e poi dirci che è più leggibile, perché non lo è.
user441521

5

Molte buone ragioni su come appare il codice . Ma per quanto riguarda i risultati ?

Diamo un'occhiata al codice C # e al suo modulo compilato IL:


using System;

public class Test {
    public static void Main(string[] args) {
        if (args.Length == 0) return;
        if ((args.Length+2)/3 == 5) return;
        Console.WriteLine("hey!!!");
    }
}

Questo semplice frammento può essere compilato. È possibile aprire il file .exe generato con ildasm e verificare qual è il risultato. Non posterò tutto ciò che è assemblatore ma descriverò i risultati.

Il codice IL generato genera quanto segue:

  1. Se la prima condizione è falsa, passa al codice in cui si trova la seconda.
  2. Se è vero, salta all'ultima istruzione. (Nota: l'ultima istruzione è un ritorno).
  3. Nella seconda condizione lo stesso accade dopo che il risultato è stato calcolato. Confronta e: arriva a Console.WriteLine se falso o alla fine se questo è vero.
  4. Stampa il messaggio e ritorna.

Quindi sembra che il codice salterà alla fine. E se facciamo un normale se con codice nidificato?

using System;

public class Test {
    public static void Main(string[] args) {
        if (args.Length != 0 && (args.Length+2)/3 != 5) 
        {
            Console.WriteLine("hey!!!");
        }
    }
}

I risultati sono abbastanza simili nelle istruzioni IL. La differenza è che prima c'erano dei salti per condizione: se falso vai al pezzo di codice successivo, se vero vai alla fine. E ora il codice IL scorre meglio e ha 3 salti (il compilatore lo ha ottimizzato un po '): 1. Primo salto: quando Lunghezza è 0 in una parte in cui il codice salta di nuovo (Terzo salto) fino alla fine. 2. Secondo: nel mezzo della seconda condizione per evitare un'istruzione. 3. Terzo: se la seconda condizione è falsa, passa alla fine.

Ad ogni modo, il contatore dei programmi salterà sempre.


5
Questa è una buona informazione, ma personalmente non me ne può fregare di meno se IL "fluisce meglio" perché le persone che gestiranno il codice non vedranno IL
JohnIdol

1
Non puoi davvero prevedere come funzionerà l'ottimizzazione nel prossimo aggiornamento del compilatore C # rispetto a ciò che vedi oggi. Personalmente, non passerei NESSUN tempo a cercare di modificare il codice C # per produrre un risultato diverso in IL, a meno che tesing non mostri che hai un problema di prestazioni SEVERE in qualche circuito stretto da qualche parte e che hai esaurito altre idee . Anche allora, penserei di più ad altre opzioni. Allo stesso modo, dubito che tu abbia idea di che tipo di ottimizzazioni stia facendo il compilatore JIT con l'IL che viene alimentato per ogni piattaforma ...
Craig

5

In teoria, l'inversione ifpotrebbe portare a prestazioni migliori se aumenta il tasso di successo della previsione del ramo. In pratica, penso che sia molto difficile sapere esattamente come si comporteranno le previsioni del ramo, specialmente dopo la compilazione, quindi non lo farei nel mio sviluppo quotidiano, tranne se scrivo il codice assembly.

Maggiori informazioni sulla previsione delle filiali qui .


4

Questo è semplicemente controverso. Non esiste un "accordo tra i programmatori" sulla questione del rientro anticipato. È sempre soggettivo, per quanto ne so.

È possibile fare un argomento di prestazione, poiché è meglio avere condizioni scritte in modo che siano spesso vere; si può anche sostenere che è più chiaro. D'altra parte, crea test nidificati.

Non credo che otterrai una risposta definitiva a questa domanda.


Non capisco il tuo argomento di prestazione. Le condizioni sono booleane, quindi qualunque sia il risultato, ci sono sempre due risultati ... Non vedo perché invertire (o meno) un'affermazione cambierebbe un risultato. (A meno che tu non stia dicendo che l'aggiunta di un "NOT" alla condizione aggiunge una quantità misurabile di elaborazione ...
Oli,

2
L'ottimizzazione funziona in questo modo: se si assicura che il caso più comune si trovi nel blocco if anziché nel blocco else, la CPU di solito avrà già le istruzioni del blocco if caricato nella propria pipeline. Se la condizione restituisce falso, la CPU deve svuotare la pipeline e ...
Altri

Mi piace anche fare ciò che gli altri dicono solo per leggibilità, odio avere casi negativi nel blocco "then" piuttosto che nell'altro. Ha senso farlo anche senza sapere o considerare cosa sta facendo la CPU
Andrew Bullock,

3

Ci sono già molte risposte perspicaci, ma vorrei comunque indirizzarmi verso una situazione leggermente diversa: invece di un presupposto, che dovrebbe essere messo in cima a una funzione, pensa a un'inizializzazione passo-passo, dove controllare che ogni passaggio abbia esito positivo e quindi passare al successivo. In questo caso, non puoi controllare tutto in alto.

Ho trovato il mio codice davvero illeggibile quando ho scritto un'applicazione host ASIO con l'ASIOSDK di Steinberg, mentre seguivo il paradigma di nidificazione. Sono passati otto livelli di profondità e non riesco a vedere un difetto di progettazione lì, come menzionato da Andrew Bullock sopra. Certo, avrei potuto impacchettare del codice interno in un'altra funzione e quindi annidare i livelli rimanenti lì per renderlo più leggibile, ma questo mi sembra piuttosto casuale.

Sostituendo l'annidamento con clausole di guardia, ho anche scoperto un mio malinteso su una parte del codice di pulizia che avrebbe dovuto verificarsi molto prima all'interno della funzione anziché alla fine. Con i rami nidificati, non l'avrei mai visto, potresti anche dire che hanno portato al mio malinteso.

Quindi questa potrebbe essere un'altra situazione in cui ifs invertiti possono contribuire a un codice più chiaro.


3

Evitare più punti di uscita può portare a miglioramenti delle prestazioni. Non sono sicuro di C # ma in C ++ l'ottimizzazione del valore restituito con nome (Copy Elision, ISO C ++ '03 12.8 / 15) dipende dall'avere un singolo punto di uscita. Questa ottimizzazione evita che la copia costruisca il valore restituito (nell'esempio specifico non importa). Ciò potrebbe portare a notevoli miglioramenti delle prestazioni in cicli stretti, poiché si salva un costruttore e un distruttore ogni volta che viene invocata la funzione.

Ma per il 99% dei casi, salvare le chiamate aggiuntive di costruttore e distruttore non vale la perdita di leggibilità ifintrodotta dai blocchi nidificati (come altri hanno sottolineato).


2
Là. mi ci sono volute 3 lunghe letture di decine di risposte in 3 domande che ponevano la stessa cosa (2 delle quali congelate), per trovare finalmente qualcuno che menzionasse NRVO. Accidenti ... grazie.
v.

2

È una questione di opinione.

Il mio approccio normale sarebbe quello di evitare ifs a riga singola e ritorni nel mezzo di un metodo.

Non vorresti linee come suggerisce ovunque nel tuo metodo, ma c'è qualcosa da dire per controllare un mucchio di ipotesi nella parte superiore del tuo metodo e fare il tuo vero lavoro solo se tutti passano.


2

A mio avviso, il ritorno anticipato va bene se stai solo restituendo void (o qualche codice di ritorno inutile che non controllerai mai) e potrebbe migliorare la leggibilità perché eviti l'annidamento e allo stesso tempo rendi esplicito che la tua funzione è svolta.

Se stai effettivamente restituendo un returnValue - l'annidamento è di solito un modo migliore per andare perché restituisci il returnValue solo in un posto (alla fine - duh) e potrebbe rendere il tuo codice più gestibile in molti casi.


1

Non sono sicuro, ma penso che R # cerchi di evitare salti lontani. Quando hai IF-ELSE, il compilatore fa qualcosa del genere:

Condizione false -> salto in lungo a false_condition_label

true_condition_label: istruzione1 ... istruzione_n

false_condition_label: istruzione1 ... istruzione_n

blocco finale

Se la condizione è vera, non vi è alcun salto e nessuna distribuzione della cache L1, ma il salto a false_condition_label può essere molto lontano e il processore deve implementare la propria cache. La sincronizzazione della cache è costosa. R # prova a sostituire i salti lontani in salti brevi e in questo caso vi è una maggiore probabilità che tutte le istruzioni siano già nella cache.


0

Penso che dipenda da ciò che preferisci, come detto, non c'è nessun accordo generale dopo. Per ridurre il fastidio, è possibile ridurre questo tipo di avviso a "Suggerimento"


0

La mia idea è che il ritorno "nel mezzo di una funzione" non dovrebbe essere così "soggettivo". Il motivo è abbastanza semplice, prendi questo codice:

    funzione do_something (data) {

      if (! is_valid_data (data)) 
            restituire false;


       do_something_that_take_an_hour (dati);

       istanza = new object_with_very_painful_constructor (dati);

          if (istanza non è valida) {
               messaggio di errore( );
                ritorno ;

          }
       connect_to_database ();
       get_some_other_data ();
       ritorno;
    }

Forse il primo "ritorno" non è COSÌ intuitivo, ma è davvero salva. Ci sono troppe "idee" sui codici puliti, che hanno semplicemente bisogno di più pratica per perdere le loro cattive idee "soggettive".


Con una lingua di eccezione non hai questi problemi.
user441521

0

Ci sono molti vantaggi in questo tipo di codifica, ma per me la grande vittoria è che se riesci a tornare rapidamente puoi migliorare la velocità della tua applicazione. IE So che grazie alla Precondizione X posso tornare rapidamente con un errore. Questo elimina prima i casi di errore e riduce la complessità del codice. In molti casi, poiché ora la pipeline della cpu può essere più pulita, può arrestare gli arresti o gli interruzioni della pipeline. In secondo luogo, se sei in un ciclo, interrompere o tornare rapidamente può farti risparmiare un sacco di CPU. Alcuni programmatori utilizzano invarianti di loop per eseguire questo tipo di uscita rapida, ma in questo caso è possibile interrompere la pipeline della CPU e persino creare problemi di ricerca della memoria e indicare che la CPU deve essere caricata dalla cache esterna. Ma fondamentalmente penso che dovresti fare quello che volevi, vale a dire che il ciclo o la funzione non creano un percorso di codice complesso solo per implementare una nozione astratta di codice corretto. Se l'unico strumento che hai è un martello, allora tutto sembra un chiodo.


Leggendo la risposta di Graffic, sembra piuttosto che il codice nidificato sia stato ottimizzato dal compilatore in modo che il codice eseguito sia più veloce rispetto all'utilizzo di più ritorni. Tuttavia, anche se i ritorni multipli fossero più veloci, questa ottimizzazione probabilmente non accelererà così tanto la tua applicazione ... :)
hangy

1
Non sono d'accordo: la prima legge riguarda la corretta correzione dell'algoritmo. Ma siamo tutti ingegneri, che si tratta di risolvere il problema corretto con le risorse che abbiamo e fare nel budget disponibile. Un'applicazione troppo lenta non è adatta allo scopo.
David Allan Finch,
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.