Una funzione dovrebbe avere solo un'istruzione return?


780

Ci sono buoni motivi per cui è meglio avere una sola dichiarazione di ritorno in una funzione?

Oppure va bene tornare da una funzione non appena è logicamente corretto farlo, nel senso che potrebbero esserci molte dichiarazioni di ritorno nella funzione?


25
Non sono d'accordo sul fatto che la domanda sia agnostica sulla lingua. Con alcune lingue, avere più ritorni è più naturale e conveniente rispetto ad altri. Sarei più propenso a lamentarmi dei primi ritorni in una funzione C che in una C ++ che utilizza RAII.
Adrian McCarthy,

3
Questo è strettamente correlato e ha risposte eccellenti: programmers.stackexchange.com/questions/118703/…
Tim Schmelter,

indipendente dal linguaggio? Spiega a qualcuno che usa un linguaggio funzionale che deve usare un ritorno per funzione: p
Boiethios

Risposte:


741

Ho spesso diverse dichiarazioni all'inizio di un metodo per tornare a situazioni "facili". Ad esempio, questo:

public void DoStuff(Foo foo)
{
    if (foo != null)
    {
        ...
    }
}

... può essere reso più leggibile (IMHO) in questo modo:

public void DoStuff(Foo foo)
{
    if (foo == null) return;

    ...
}

Quindi sì, penso che vada bene avere più "punti di uscita" da una funzione / metodo.


83
Concordato. Anche se avere più punti di uscita può sfuggire di mano, penso che sia meglio che mettere l'intera funzione in un blocco IF. Usa return ogni volta che ha senso mantenere il tuo codice leggibile.
Joshua Carmody,

172
Questa è nota come "dichiarazione di guardia" è il refactoring di Fowler.
Lars Westergren,

12
Quando le funzioni sono relativamente brevi, non è difficile seguire la struttura di una funzione con un punto di ritorno vicino al centro.
KJA Wolf,

21
Enorme blocco di istruzioni if-else, ognuna con un ritorno? Non è niente. Una cosa del genere è di solito facilmente riformulata. (almeno la variazione di uscita singola più comune con una variabile di risultato è, quindi dubito che la variazione di uscita multipla sarebbe più difficile.) Se vuoi un vero mal di testa, guarda una combinazione di if-else e while loop (controllato da local booleani), dove vengono impostate le variabili dei risultati, portando a un punto di uscita finale alla fine del metodo. Questa è l'idea dell'uscita singola impazzita, e sì, sto parlando per esperienza reale avendo dovuto affrontarla.
Marcus Andrén,

7
'Immagina: è necessario eseguire il metodo "IncreaseStuffCallCounter" alla fine della funzione' DoStuff '. Cosa farai in questo caso? :) '-DoStuff() { DoStuffInner(); IncreaseStuffCallCounter(); }
Jim Balter,

355

Nessuno ha menzionato o citato il codice completo, quindi lo farò.

17.1 ritorno

Ridurre al minimo il numero di resi in ciascuna routine . È più difficile capire una routine se, leggendola in fondo, non si è consapevoli della possibilità che sia tornata da qualche parte sopra.

Utilizzare un ritorno quando migliora la leggibilità . In alcune routine, una volta che conosci la risposta, vuoi riportarla immediatamente alla routine di chiamata. Se la routine è definita in modo tale da non richiedere alcuna pulizia, non tornare immediatamente significa che devi scrivere più codice.


64
+1 per la sfumatura di "minimizza" ma non proibisce resi multipli.
Raedwald,

13
"più difficile da capire" è molto soggettivo, specialmente quando l'autore non fornisce alcuna prova empirica a supporto dell'affermazione generale ... avere una singola variabile che deve essere impostata in molte posizioni condizionali nel codice per la dichiarazione di ritorno finale è ugualmente soggetta a "ignaro della possibilità che la variabile fosse assegnata da qualche parte sopra nella sua funzione!
Heston T. Holtmann,

26
Personalmente, preferisco tornare presto dove possibile. Perché? Bene, quando vedi la parola chiave return per un determinato caso, sai immediatamente "Ho finito" - non devi continuare a leggere per capire cosa succede, se non altro, in seguito.
Mark Simpson,

12
@ HestonT.Holtmann: La cosa che ha reso Code Complete unica tra i libri di programmazione è che il consiglio è supportato da prove empiriche.
Adrian McCarthy,

9
Questa dovrebbe probabilmente essere la risposta accettata, dal momento che menziona che avere più punti di ritorno non è sempre buono, ma a volte necessario.
Rafid

229

Direi che sarebbe incredibilmente poco saggio decidere arbitrariamente contro più punti di uscita poiché ho trovato la tecnica utile nella pratica più e più volte , in effetti ho spesso rifatto il codice esistente in più punti di uscita per maggiore chiarezza. Possiamo confrontare i due approcci in questo modo: -

string fooBar(string s, int? i) {
  string ret = "";
  if(!string.IsNullOrEmpty(s) && i != null) {
    var res = someFunction(s, i);

    bool passed = true;
    foreach(var r in res) {
      if(!r.Passed) {
        passed = false;
        break;
      }
    }

    if(passed) {
      // Rest of code...
    }
  }

  return ret;
}

Confrontalo con il codice in cui sono consentiti più punti di uscita : -

string fooBar(string s, int? i) {
  var ret = "";
  if(string.IsNullOrEmpty(s) || i == null) return null;

  var res = someFunction(s, i);

  foreach(var r in res) {
      if(!r.Passed) return null;
  }

  // Rest of code...

  return ret;
}

Penso che quest'ultimo sia notevolmente più chiaro. Per quanto ne so, la critica di più punti di uscita è un punto di vista piuttosto arcaico in questi giorni.


12
La chiarezza è negli occhi di chi guarda: guardo una funzione e cerco un inizio, una metà e una fine. Quando la funzione è piccola va bene, ma quando stai cercando di capire perché qualcosa non funziona e "Resto del codice" risulta non banale, puoi passare anni a cercare perché ret è
Murph,

7
Prima di tutto, questo è un esempio inventato. In secondo luogo, dove: string ret; "andare nella seconda versione? In terzo luogo, Ret non contiene informazioni utili. In quarto luogo, perché così tanta logica in una funzione / metodo? In quinto luogo, perché non rendere DidValuesPass (tipo res) quindi RestOfCode () separato
funzioni

25
@Rick 1. Non inventato nella mia esperienza, questo è in realtà uno schema che ho incontrato molte volte, 2. È assegnato in "resto del codice", forse non è chiaro. 3. Ehm? È un esempio? 4. Beh, immagino che sia inventato sotto questo aspetto, ma è possibile giustificarlo così tanto, 5. potrebbe fare ...
ljs

5
@Rick il punto è che spesso è più facile tornare presto piuttosto che concludere il codice in un'enorme istruzione if. Viene fuori molto dalla mia esperienza, anche con un adeguato refactoring.
ljs

5
Il punto di non avere più dichiarazioni di ritorno è semplificare il debug, un secondo è per la leggibilità. Un punto di interruzione alla fine ti consente di vedere il valore di uscita, senza eccezioni. Per quanto riguarda la leggibilità, le 2 funzioni mostrate non si comportano allo stesso modo. Il ritorno predefinito è una stringa vuota se! R.Passed ma quello "più leggibile" lo cambia per restituire null. L'autore ha letto male che in precedenza c'era un default dopo solo poche righe. Anche in esempi banali è facile avere rendimenti predefiniti poco chiari, qualcosa che un singolo ritorno alla fine aiuta a far rispettare.
Mitch,

191

Attualmente sto lavorando a una base di codice in cui due delle persone che ci lavorano sottoscrivono ciecamente la teoria del "punto unico di uscita" e posso dirti che per esperienza è una pratica orribile orribile. Rende il codice estremamente difficile da mantenere e ti mostrerò il perché.

Con la teoria del "singolo punto di uscita", inevitabilmente ti ritrovi con un codice simile al seguente:

function()
{
    HRESULT error = S_OK;

    if(SUCCEEDED(Operation1()))
    {
        if(SUCCEEDED(Operation2()))
        {
            if(SUCCEEDED(Operation3()))
            {
                if(SUCCEEDED(Operation4()))
                {
                }
                else
                {
                    error = OPERATION4FAILED;
                }
            }
            else
            {
                error = OPERATION3FAILED;
            }
        }
        else
        {
            error = OPERATION2FAILED;
        }
    }
    else
    {
        error = OPERATION1FAILED;
    }

    return error;
}

Non solo questo rende il codice molto difficile da seguire, ma ora dico in seguito che è necessario tornare indietro e aggiungere un'operazione tra 1 e 2. Devi rientrare a circa l'intera funzione di eccitazione e buona fortuna assicurandoti che tutto le condizioni if ​​/ else e le parentesi graffe sono abbinate correttamente.

Questo metodo rende la manutenzione del codice estremamente difficile e soggetta a errori.


5
@Murph: non hai modo di sapere che non si verifica nient'altro dopo ogni condizione senza leggerne una. Normalmente direi che questi tipi di argomenti sono soggettivi, ma questo è chiaramente sbagliato. Con un ritorno su ogni errore, hai finito, sai esattamente cosa è successo.
GEOCHET,

6
@Murph: ho visto questo tipo di codice usato, abusato e abusato. L'esempio è abbastanza semplice, in quanto non vi è vero se / else all'interno. Tutto ciò che questo tipo di codice deve abbattere è un "altro" dimenticato. AFAIK, questo codice ha davvero bisogno di eccezioni.
Paercebal,

15
Puoi trasformarlo in questo, mantenere la sua "purezza": if (! SUCCEEDED (Operation1 ())) {} else error = OPERATION1FAILED; if (errore! = S_OK) {if (SUCCEEDED (Operation2 ())) {} else error = OPERATION2FAILED; } if (errore! = S_OK) {if (SUCCEEDED (Operation3 ())) {} else error = OPERATION3FAILED; } //eccetera.
Joe Pineda,

6
Questo codice non ha un solo punto di uscita: ognuna di quelle istruzioni "error =" si trova su un percorso di uscita. Non si tratta solo di uscire dalle funzioni, ma di uscire da qualsiasi blocco o sequenza.
Jay Bazuzi,

6
Non sono d'accordo sul fatto che il singolo ritorno "inevitabilmente" si traduca in una nidificazione profonda. Il tuo esempio può essere scritto come una funzione semplice e lineare con un singolo ritorno (senza goto). E se non fai o non puoi fare affidamento esclusivamente su RAII per la gestione delle risorse, i ritorni iniziali finiscono per causare perdite o codice duplicato. Ancora più importante, i ritorni anticipati rendono poco pratico affermare le condizioni post.
Adrian McCarthy,

72

La programmazione strutturata dice che dovresti avere sempre e solo una dichiarazione di ritorno per funzione. Questo per limitare la complessità. Molte persone come Martin Fowler sostengono che è più semplice scrivere funzioni con più dichiarazioni di ritorno. Presenta questo argomento nel classico libro di refactoring che ha scritto. Funziona bene se segui i suoi altri consigli e scrivi piccole funzioni. Sono d'accordo con questo punto di vista e solo i puristi di una programmazione strutturata rigorosa aderiscono a dichiarazioni di ritorno singole per funzione.


44
La programmazione strutturata non dice nulla. Lo dicono alcune (ma non tutte) persone che si descrivono come sostenitori della programmazione strutturata.
wnoise il

15
"Funziona bene se segui i suoi altri consigli e scrivi piccole funzioni." Questo è il punto più importante. Le piccole funzioni richiedono raramente molti punti di ritorno.

6
@wnoise +1 per il commento, così vero. Tutta la "programmazione strutturata" dice che non usare GOTO.
paxos1977,

1
@ceretullis: a meno che non sia necessario. Naturalmente non è essenziale, ma può essere utile in C. Il kernel di Linux lo usa, e per buoni motivi. GOTO ha considerato dannoso parlare dell'utilizzo GOTOper spostare il flusso di controllo anche quando esisteva una funzione. Non dice mai "non usare mai GOTO".
Esteban Küber,

1
"si tratta solo di ignorare la" struttura "del codice." - No, al contrario. "ha senso dire che dovrebbero essere evitati" - no, non lo è.
Jim Balter,

62

Come osserva Kent Beck quando si discute delle clausole di guardia nei modelli di implementazione che fanno di una routine un unico punto di entrata e uscita ...

"era per evitare la confusione possibile quando si entrava e usciva da molte località nella stessa routine. Aveva senso se applicato a FORTRAN o ai programmi di linguaggio assembly scritti con molti dati globali in cui anche capire quali dichiarazioni venivano eseguite era un duro lavoro. con metodi piccoli e dati prevalentemente locali, è inutilmente conservativo ".

Trovo che una funzione scritta con clausole di guardia sia molto più facile da seguire rispetto a un lungo gruppo di if then elsedichiarazioni annidate .


Naturalmente, "un lungo gruppo annidato di dichiarazioni if-then-else" non è l'unica alternativa alle clausole di guardia.
Adrian McCarthy,

@AdrianMcCarthy hai un'alternativa migliore? Sarebbe più utile del sarcasmo.
shinzou,

@kuhaku: Non sono sicuro di chiamare quel sarcasmo. La risposta suggerisce che si tratta di una o / o situazione: clausole di guardia o gruppi nidificati a lungo di if-then-else. Molti (la maggior parte?) Linguaggi di programmazione offrono molti modi per tener conto di tale logica oltre alle clausole di guardia.
Adrian McCarthy,

61

In una funzione che non ha effetti collaterali, non ci sono buoni motivi per avere più di un singolo ritorno e dovresti scriverli in uno stile funzionale. In un metodo con effetti collaterali, le cose sono più sequenziali (indicizzate nel tempo), quindi scrivi in ​​uno stile imperativo, usando l'istruzione return come comando per interrompere l'esecuzione.

In altre parole, quando possibile, favorisci questo stile

return a > 0 ?
  positively(a):
  negatively(a);

oltre questo

if (a > 0)
  return positively(a);
else
  return negatively(a);

Se ti ritrovi a scrivere diversi livelli di condizioni nidificate, probabilmente c'è un modo per fare il refactoring, usando ad esempio l'elenco dei predicati. Se scopri che i tuoi if e else sono molto distanti sintatticamente, potresti voler scomporlo in funzioni più piccole. Un blocco condizionale che si estende su più di una schermata di testo è difficile da leggere.

Non esiste una regola rigida che si applica a tutte le lingue. Qualcosa come avere una singola dichiarazione di ritorno non renderà il tuo codice buono. Ma un buon codice tenderà a permetterti di scrivere le tue funzioni in quel modo.


6
+1 "Se scopri che i tuoi if ed elts sono molto distanti sintatticamente, potresti voler scomporlo in funzioni più piccole."
Andres Jaan Tack,

4
+1, se questo è un problema, di solito significa che stai facendo troppo in una singola funzione. Mi deprime davvero che questa non è la risposta più votata
Matt Briggs,

1
Le dichiarazioni della guardia non hanno effetti collaterali, ma la maggior parte delle persone le considererebbe utili. Quindi potrebbero esserci motivi per interrompere l'esecuzione anticipata anche se non ci sono effetti collaterali. Questa risposta non risolve completamente il problema secondo me.
Maarten Bodewes,

@ MaartenBodewes-owlstead Vedi "Rigidità e pigrizia"
Apocalisp

43

L'ho visto negli standard di codifica per C ++ che erano un hang-over da C, come se non avessi RAII o altra gestione automatica della memoria, quindi devi ripulire per ogni ritorno, il che significa che taglia e incolla del clean-up o di un goto (logicamente lo stesso di "finally" nelle lingue gestite), entrambi considerati non validi. Se la tua pratica è quella di utilizzare puntatori e raccolte intelligenti in C ++ o in un altro sistema di memoria automatico, allora non c'è un motivo valido per farlo, e diventa tutto una questione di leggibilità e più di una richiesta di giudizio.


Ben detto, anche se credo che sia meglio copiare le eliminazioni quando si cerca di scrivere codice altamente ottimizzato (come il software skinning complesse mesh 3d!)
Grant Peters

1
Cosa ti fa credere? Se si dispone di un compilatore con scarsa ottimizzazione in cui vi è un certo sovraccarico nel dereferenziamento di auto_ptr, è possibile utilizzare puntatori semplici in parallelo. Anche se sarebbe strano scrivere codice "ottimizzato" con un compilatore non ottimizzante in primo luogo.
Pete Kirkham,

Ciò costituisce un'interessante eccezione alla regola: se il tuo linguaggio di programmazione non contiene qualcosa che viene chiamato automaticamente alla fine del metodo (come try... finallyin Java) e devi fare la manutenzione delle risorse che potresti fare con un singolo ritorna alla fine di un metodo. Prima di farlo, dovresti prendere in seria considerazione il refactoring del codice per sbarazzarti della situazione.
Maarten Bodewes,

@PeteKirkham perché goto for cleanup è male? sì, goto può essere usato male, ma questo particolare utilizzo non è male.
q126y,

1
@ q126y in C ++, a differenza di RAII, non riesce quando viene generata un'eccezione. In C, è una pratica perfettamente valida. Vedi stackoverflow.com/questions/379172/use-goto-or-not
Pete Kirkham,

40

Mi affido all'idea che le dichiarazioni di ritorno nel mezzo della funzione siano cattive. È possibile utilizzare i ritorni per creare alcune clausole di protezione nella parte superiore della funzione e, naturalmente, dire al compilatore cosa restituire alla fine della funzione senza problemi, ma i ritorni nel mezzo della funzione possono essere facili da perdere e possono rendere la funzione più difficile da interpretare.


38

Ci sono buoni motivi per cui è meglio avere una sola dichiarazione di ritorno in una funzione?

, ci sono:

  • Il singolo punto di uscita offre un luogo eccellente per affermare le tue condizioni post.
  • Essere in grado di mettere un punto di interruzione del debugger su un ritorno alla fine della funzione è spesso utile.
  • Meno ritorni significa meno complessità. Il codice lineare è generalmente più semplice da capire.
  • Se tentare di semplificare una funzione per un singolo ritorno causa complessità, questo è un incentivo a ricorrere a funzioni più piccole, più generali e più comprensibili.
  • Se sei in una lingua senza distruttori o se non usi RAII, un singolo ritorno riduce il numero di posti che devi ripulire.
  • Alcune lingue richiedono un unico punto di uscita (ad es. Pascal ed Eiffel).

La domanda viene spesso posta come una falsa dicotomia tra rendimenti multipli o dichiarazioni nidificate in profondità. C'è quasi sempre una terza soluzione che è molto lineare (nessun annidamento profondo) con un solo punto di uscita.

Aggiornamento : apparentemente le linee guida MISRA promuovono anche l'uscita singola .

Per essere chiari, non sto dicendo che è sempre sbagliato avere più ritorni. Ma date soluzioni altrimenti equivalenti, ci sono molte buone ragioni per preferire quella con un solo ritorno.


2
un altro buon motivo, probabilmente il migliore in questi giorni, per avere una singola dichiarazione di ritorno è la registrazione. se si desidera aggiungere la registrazione a un metodo, è possibile inserire una singola istruzione di registro che trasmetta ciò che restituisce il metodo.
o

Quanto era comune l'istruzione FORTRAN ENTRY? Vedi docs.oracle.com/cd/E19957-01/805-4939/6j4m0vn99/index.html . E se sei avventuroso, puoi registrare i metodi con AOP e post-consulenza
Eric Jablow,

1
+1 I primi 2 punti sono stati sufficienti per convincermi. Quello con il secondo ultimo paragrafo. Non sarei in disaccordo con l'elemento di registrazione per lo stesso motivo per cui scoraggio i condizionali nidificati profondi poiché incoraggiano a infrangere la regola della singola responsabilità, che è la ragione principale per cui il polimorfismo è stato introdotto in OOP.
Francis Rodgers,

Vorrei solo aggiungere che con Contratti C # e Codice, il problema post-condizione non è un problema, poiché è ancora possibile utilizzare Contract.Ensurescon più punti di ritorno.
julealgon,

1
@ q126y: se stai usando il gotocodice di pulizia comune, probabilmente hai semplificato la funzione in modo che returnalla fine del codice di pulizia sia presente un singolo . Quindi potresti dire di aver risolto il problemagoto , ma direi che l'hai risolto semplificando in un singolo return.
Adrian McCarthy,

33

Avere un singolo punto di uscita offre un vantaggio nel debug, poiché consente di impostare un singolo punto di interruzione alla fine di una funzione per vedere quale valore verrà effettivamente restituito.


6
BRAVO! Sei l' unica persona a menzionare questa ragione oggettiva . È il motivo per cui preferisco punti di uscita singoli rispetto a più punti di uscita. Se il mio debugger potesse impostare un punto di interruzione su qualsiasi punto di uscita, preferirei probabilmente più punti di uscita. La mia opinione attuale è che le persone che codificano più punti di uscita lo fanno a proprio vantaggio a spese di quegli altri che devono usare un debugger sul loro codice (e sì, sto parlando di tutti voi collaboratori open source che scrivono codice con punti di uscita multipli.)
MikeSchinkel

3
SÌ. Sto aggiungendo il codice di registrazione a un sistema che si comporta in modo intermittente in fase di produzione (dove non riesco a passare). Se il programmatore precedente avesse usato l'uscita singola, sarebbe stato MOLTO più semplice.
Michael Blackburn,

3
È vero, nel debug è utile. Ma in pratica nella maggior parte dei casi sono stato in grado di impostare il punto di interruzione nella funzione di chiamata, subito dopo la chiamata, in modo efficace allo stesso risultato. (E quella posizione si trova nello stack di chiamate, ovviamente.) YMMV.
foo

Questo a meno che il tuo debugger non fornisca una funzione step-out o step-return (e ogni debugger lo fa per quanto ne so), che mostra il valore restituito subito dopo la restituzione. La modifica del valore in seguito può essere un po 'complicata se non viene assegnata a una variabile.
Maarten Bodewes,

7
Non vedo un debugger da molto tempo che non ti permetterebbe di impostare un breakpoint sulla "chiusura" del metodo (fine, parentesi graffa destra, qualunque sia la tua lingua) e colpire quel breakpoint indipendentemente da dove o da quanti , i statemets di ritorno sono nel metodo. Inoltre, anche se la tua funzione ha un solo ritorno, ciò non significa che non puoi uscire dalla funzione con un'eccezione (esplicita o ereditata). Quindi, penso che questo non sia davvero un punto valido.
Scott Gartner,

19

In generale, provo ad avere un solo punto di uscita da una funzione. Ci sono volte, tuttavia, che in questo modo si finisce per creare un corpo funzionale più complesso di quanto sia necessario, nel qual caso è meglio avere più punti di uscita. Deve essere davvero un "appello di giudizio" basato sulla complessità risultante, ma l'obiettivo dovrebbe essere il minor numero possibile di punti di uscita senza sacrificare la complessità e la comprensibilità.


"In generale, provo ad avere un solo punto di uscita da una funzione" - perché? "l'obiettivo dovrebbe essere il minor numero possibile di punti di uscita" - perché? E perché 19 persone hanno votato questa non risposta?
Jim Balter,

@JimBalter In definitiva, si riduce alle preferenze personali. Più punti di uscita in genere portano a un metodo più complesso (anche se non sempre) e rendono più difficile la comprensione da parte di qualcuno.
Scott Dorman,

"si riduce alle preferenze personali". In altre parole, non puoi offrire un motivo. "Più punti di uscita in genere portano a un metodo più complesso (anche se non sempre)" - No, in realtà non lo fanno. Date due funzioni logicamente equivalenti, una con clausole di guardia e una con uscita singola, quest'ultima avrà una maggiore complessità ciclomatica, che numerosi studi mostrano risultati in codice più inclini all'errore e difficili da comprendere. Potresti trarre vantaggio dalla lettura delle altre risposte qui.
Jim Balter,

14

No, perché non viviamo più negli anni '70 . Se la tua funzione è abbastanza lunga da rendere più ritorni multipli, è troppo lunga.

(A parte il fatto che qualsiasi funzione multilinea in una lingua con eccezioni avrà comunque più punti di uscita.)


14

La mia preferenza sarebbe per l'uscita singola a meno che non complichi davvero le cose. Ho scoperto che in alcuni casi, più punti esistenti possono mascherare altri problemi di progettazione più significativi:

public void DoStuff(Foo foo)
{
    if (foo == null) return;
}

Vedendo questo codice, chiederei immediatamente:

  • "Pippo" è mai nullo?
  • In tal caso, quanti client di "DoStuff" hanno mai chiamato la funzione con un "pippo" nullo?

A seconda delle risposte a queste domande potrebbe essere quello

  1. il controllo è inutile in quanto non è mai vero (cioè dovrebbe essere un'asserzione)
  2. il controllo è molto raramente vero e quindi potrebbe essere meglio cambiare quelle specifiche funzioni del chiamante in quanto dovrebbero comunque intraprendere qualche altra azione.

In entrambi i casi di cui sopra, il codice può essere probabilmente rielaborato con un'asserzione per garantire che "pippo" non sia mai nullo e che i relativi chiamanti siano cambiati.

Ci sono altri due motivi (specifico penso al codice C ++) in cui esiste più multiplo in realtà può avere un negativo effetto . Sono dimensioni del codice e ottimizzazioni del compilatore.

Un oggetto C ++ non POD in ambito all'uscita di una funzione avrà il suo distruttore chiamato. Laddove ci sono diverse dichiarazioni di ritorno, può accadere che ci siano diversi oggetti nell'ambito e quindi l'elenco dei distruttori da chiamare sarà diverso. Il compilatore deve quindi generare codice per ogni istruzione return:

void foo (int i, int j) {
  A a;
  if (i > 0) {
     B b;
     return ;   // Call dtor for 'b' followed by 'a'
  }
  if (i == j) {
     C c;
     B b;
     return ;   // Call dtor for 'b', 'c' and then 'a'
  }
  return 'a'    // Call dtor for 'a'
}

Se la dimensione del codice è un problema, allora potrebbe essere qualcosa che vale la pena evitare.

L'altro problema riguarda "OptimiZation del valore di ritorno denominato" (aka Copy Elision, ISO C ++ '03 12.8 / 15). C ++ consente a un'implementazione di saltare la chiamata al costruttore della copia se può:

A foo () {
  A a1;
  // do something
  return a1;
}

void bar () {
  A a2 ( foo() );
}

Prendendo il codice così com'è, l'oggetto 'a1' è costruito in 'pippo' e quindi il suo costrutto copia sarà chiamato per costruire 'a2'. Tuttavia, copia elisione consente al compilatore di costruire 'a1' nello stesso posto nello stack di 'a2'. Non è quindi necessario "copiare" l'oggetto quando la funzione ritorna.

Punti di uscita multipli complicano il lavoro del compilatore nel tentativo di rilevare questo, e almeno per una versione relativamente recente di VC ++ l'ottimizzazione non ha avuto luogo in cui il corpo della funzione aveva più ritorni. Vedere Ottimizzazione del valore restituito con nome in Visual C ++ 2005 per ulteriori dettagli.


1
Se prendi tutto tranne l'ultimo dtor dal tuo esempio C ++, il codice per distruggere B e successivamente C e B, dovrà comunque essere generato quando termina l'ambito dell'istruzione if, quindi non guadagni davvero nulla senza avere più ritorni .
Eclipse,

4
+1 E waaaay in fondo all'elenco abbiamo la REALE ragione per cui esiste questa pratica di codifica - NRVO. Tuttavia, questa è una micro-ottimizzazione; e, come tutte le pratiche di micro-ottimizzazione, è stato probabilmente avviato da un "esperto" di 50 anni che è abituato a programmare su un PDP-8 a 300 kHz e non comprende l'importanza del codice pulito e strutturato. In generale, segui i consigli di Chris S e usa più dichiarazioni di reso ogni volta che ha senso.
BlueRaja - Danny Pflughoeft,

Anche se non sono d'accordo con le tue preferenze (nella mia mente, il tuo suggerimento Assert è anche un punto di ritorno, come throw new ArgumentNullException()in C # in questo caso), mi sono piaciute molto le altre tue considerazioni, sono tutte valide per me e potrebbero essere critiche in alcuni contesti di nicchia.
julealgon,

Questo è pieno zeppo di uomini di paglia. La domanda sul perché fooè in fase di test non ha nulla a che fare con l'argomento, ovvero se farlo if (foo == NULL) return; dowork; oif (foo != NULL) { dowork; }
Jim Balter,

11

Avere un singolo punto di uscita riduce la complessità ciclomatica e quindi, in teoria , riduce la probabilità che tu introduca dei bug nel tuo codice quando lo cambi. La pratica, tuttavia, tende a suggerire che è necessario un approccio più pragmatico. Tendo quindi a puntare ad avere un unico punto di uscita, ma consento al mio codice di avere diversi se questo è più leggibile.


Molto perspicace. Tuttavia, ritengo che fino a quando un programmatore non saprà quando utilizzare più punti di uscita, dovrebbero essere vincolati a uno.
Rick Minerich,

5
Non proprio. La complessità ciclomatica di "if (...) return; ... return;" è uguale a "if (...) {...} return;". Entrambi hanno due percorsi attraverso di loro.
Steve Emmerson,

11

Mi costringo a usare solo returnun'istruzione, poiché in un certo senso genererà odore di codice. Lasciatemi spiegare:

function isCorrect($param1, $param2, $param3) {
    $toret = false;
    if ($param1 != $param2) {
        if ($param1 == ($param3 * 2)) {
            if ($param2 == ($param3 / 3)) {
                $toret = true;
            } else {
                $error = 'Error 3';
            }
        } else {
            $error = 'Error 2';
        }
    } else {
        $error = 'Error 1';
    }
    return $toret;
}

(Le condizioni sono arbitrarie ...)

Più condizioni, più grande diventa la funzione, più è difficile da leggere. Quindi, se sei in sintonia con l'odore del codice, te ne accorgerai e vorrai riformattare il codice. Due possibili soluzioni sono:

  • Resi multipli
  • Rifattorizzare in funzioni separate

Resi multipli

function isCorrect($param1, $param2, $param3) {
    if ($param1 == $param2)       { $error = 'Error 1'; return false; }
    if ($param1 != ($param3 * 2)) { $error = 'Error 2'; return false; }
    if ($param2 != ($param3 / 3)) { $error = 'Error 3'; return false; }
    return true;
}

Funzioni separate

function isEqual($param1, $param2) {
    return $param1 == $param2;
}

function isDouble($param1, $param2) {
    return $param1 == ($param2 * 2);
}

function isThird($param1, $param2) {
    return $param1 == ($param2 / 3);
}

function isCorrect($param1, $param2, $param3) {
    return !isEqual($param1, $param2)
        && isDouble($param1, $param3)
        && isThird($param2, $param3);
}

Certo, è più lungo e un po 'disordinato, ma nel processo di refactoring della funzione in questo modo, abbiamo

  • creato una serie di funzioni riutilizzabili,
  • ha reso la funzione più leggibile dall'uomo e
  • il focus delle funzioni è sul perché i valori sono corretti.

5
-1: cattivo esempio. Hai omesso la gestione dei messaggi di errore. Se ciò non fosse necessario, isCorrect potrebbe essere espresso come return xx && yy && zz; dove xx, yy e z sono le espressioni isEqual, isDouble e isThird.
Kauppi

10

Direi che dovresti averne tutti quelli necessari o quelli che rendono il codice più pulito (come le clausole di guardia ).

Personalmente non ho mai sentito / visto alcuna "best practice" dire che dovresti avere una sola dichiarazione di ritorno.

Per la maggior parte, tendo a uscire da una funzione il più presto possibile in base a un percorso logico (le clausole di guardia ne sono un eccellente esempio).


10

Credo che i ritorni multipli siano generalmente buoni (nel codice che scrivo in C #). Lo stile a ritorno singolo è un blocco da C. Ma probabilmente non stai codificando in C.

Non esiste una legge che richieda un solo punto di uscita per un metodo in tutti i linguaggi di programmazione . Alcune persone insistono sulla superiorità di questo stile, e talvolta lo elevano a una "regola" o "legge", ma questa convinzione non è supportata da alcuna prova o ricerca.

Più di uno stile di ritorno può essere una cattiva abitudine nel codice C, in cui le risorse devono essere esplicitamente disallocate, ma linguaggi come Java, C #, Python o JavaScript che hanno costrutti come garbage collection e try..finallyblocchi automatici (eusing blocchi in C # ) e questo argomento non si applica - in queste lingue, è molto raro avere bisogno di una deallocazione manuale centralizzata delle risorse.

Ci sono casi in cui un singolo ritorno è più leggibile e casi in cui non lo è. Verifica se riduce il numero di righe di codice, rende più chiara la logica o riduce il numero di parentesi graffe e rientri o variabili temporanee.

Pertanto, utilizza tutti i ritorni più adatti alle tue sensibilità artistiche, perché si tratta di un problema di layout e leggibilità, non tecnico.

Ne ho parlato a lungo sul mio blog .


10

Ci sono cose buone da dire sull'avere un unico punto di uscita, così come ci sono cose cattive da dire sull'inevitabile programmazione a "freccia" che ne risulta.

Se utilizzo più punti di uscita durante la convalida dell'input o l'allocazione delle risorse, provo a mettere tutte le "uscite di errore" in modo molto visibile in cima alla funzione.

Sia l' articolo di programmazione spartana di "SSDSLPedia" sia il punto di uscita a singola funzione articolo del del "Wiki del Portland Pattern Repository" hanno alcuni argomenti approfonditi al riguardo. Inoltre, ovviamente, c'è questo post da considerare.

Se vuoi davvero un singolo punto di uscita (in qualsiasi linguaggio non abilitato alle eccezioni), ad esempio per liberare risorse in un unico posto, trovo che l'applicazione attenta di goto sia buona; vedi ad esempio questo esempio piuttosto forzato (compresso per salvare lo schermo immobiliare):

int f(int y) {
    int value = -1;
    void *data = NULL;

    if (y < 0)
        goto clean;

    if ((data = malloc(123)) == NULL)
        goto clean;

    /* More code */

    value = 1;
clean:
   free(data);
   return value;
}

Personalmente, in generale, non mi piace la programmazione delle frecce più di quanto non mi piacciano più punti di uscita, sebbene entrambi siano utili se applicati correttamente. Il migliore, ovviamente, è strutturare il programma in modo da non richiedere nessuno dei due. Abbattere la funzione in più blocchi di solito aiuta :)

Anche se, facendo ciò, trovo che alla fine con più punti di uscita, come in questo esempio, in cui alcune funzioni più grandi sono state suddivise in diverse funzioni più piccole:

int g(int y) {
  value = 0;

  if ((value = g0(y, value)) == -1)
    return -1;

  if ((value = g1(y, value)) == -1)
    return -1;

  return g2(y, value);
}

A seconda del progetto o delle linee guida di codifica, la maggior parte del codice della piastra della caldaia potrebbe essere sostituito da macro. Come nota a margine, scomporlo in questo modo rende le funzioni g0, g1, g2 molto facili da testare individualmente.

Ovviamente, in un linguaggio OO e abilitato per le eccezioni, non userei istruzioni if ​​del genere (o del tutto, se potessi cavarmela con il minimo sforzo), e il codice sarebbe molto più semplice. E non frizzante. E la maggior parte dei rendimenti non finali sarebbero probabilmente eccezioni.

In breve;

  • Pochi ritorni sono migliori di molti ritorni
  • Più di un ritorno è meglio di enormi frecce e le clausole di guardia sono generalmente ok.
  • Le eccezioni potrebbero / dovrebbero probabilmente sostituire la maggior parte delle "clausole di guardia" quando possibile.

L'esempio va in crash per y <0, perché prova a liberare il puntatore NULL ;-)
Erich Kitzmueller,

2
opengroup.org/onlinepubs/009695399/functions/free.html "Se ptr è un puntatore nullo, non deve verificarsi alcuna azione."
Henrik Gustafsson,

1
No, non si arresta in modo anomalo, perché passare NULL a libero è un no-op definito. È un'idea sbagliata fastidiosamente comune che devi prima verificare NULL.
hlovdal

Il modello "a freccia" non è un'alternativa inevitabile. Questa è una falsa dicotomia.
Adrian McCarthy,

9

Conosci l'adagio: la bellezza è negli occhi di chi guarda .

Alcune persone giurano su NetBeans e altre su IntelliJ IDEA , altre su Python e altre su PHP .

In alcuni negozi potresti perdere il lavoro se insisti nel farlo:

public void hello()
{
   if (....)
   {
      ....
   }
}

La domanda riguarda la visibilità e la manutenibilità.

Sono dipendente dall'uso dell'algebra booleana per ridurre e semplificare la logica e l'uso delle macchine a stati. Tuttavia, ci sono stati colleghi precedenti che credevano che il mio impiego di "tecniche matematiche" nella programmazione non fosse adatto, perché non sarebbe stato visibile e mantenibile. E sarebbe una cattiva pratica. Mi dispiace gente, le tecniche che utilizzo sono molto visibili e mantenibili per me, perché quando tornerò al codice sei mesi dopo, avrei capito chiaramente il codice piuttosto che vedere un casino di proverbiali spaghetti.

Ehi amico (come diceva un ex cliente) fai quello che vuoi fintanto che sai come ripararlo quando ho bisogno che tu lo aggiusti.

Ricordo che 20 anni fa, un mio collega è stato licenziato per aver assunto quella che oggi sarebbe chiamata strategia di sviluppo agile . Aveva un meticoloso piano incrementale. Ma il suo manager gli stava urlando "Non puoi rilasciare in modo incrementale funzionalità agli utenti! Devi restare con la cascata ". La sua risposta al direttore fu che lo sviluppo incrementale sarebbe stato più preciso alle esigenze del cliente. Credeva nello sviluppo per le esigenze dei clienti, ma il manager credeva nella codifica in base alle "esigenze del cliente".

Siamo spesso colpevoli di infrangere la normalizzazione dei dati, i limiti MVP e MVC . Inline invece di costruire una funzione. Prendiamo le scorciatoie.

Personalmente, credo che PHP sia una cattiva pratica, ma cosa ne so. Tutti gli argomenti teorici si riducono al tentativo di soddisfare un insieme di regole

qualità = precisione, manutenibilità e redditività.

Tutte le altre regole si dissolvono in background. E ovviamente questa regola non svanisce mai:

La pigrizia è la virtù di un buon programmatore.


1
"Ehi amico (come diceva un ex cliente) fai quello che vuoi purché tu sappia come ripararlo quando ho bisogno che tu lo risolva." Problema: spesso non sei tu a "ripararlo".
Dan Barron,

+1 su questa risposta perché sono d'accordo su dove stai andando ma non necessariamente su come arrivarci. Direi che ci sono livelli di comprensione. vale a dire la comprensione che il dipendente A ha dopo 5 anni di esperienza di programmazione e 5 anni con una società è molto diverso da quello del dipendente B, un nuovo laureato che ha appena iniziato con una società. Il mio punto sarebbe che se l'impiegato A è l'unica persona in grado di comprendere il codice, NON è gestibile e quindi dovremmo tutti sforzarci di scrivere codice che l'impiegato B possa comprendere. Qui è dove l'arte vera è nel software.
Francis Rodgers,

9

Mi propongo di utilizzare le clausole di guardia per tornare presto e altrimenti uscire alla fine di un metodo. La singola regola di entrata e uscita ha un significato storico ed è stata particolarmente utile quando si ha a che fare con codice legacy che correva su 10 pagine A4 per un singolo metodo C ++ con ritorni multipli (e molti difetti). Più recentemente, la buona pratica accettata è quella di mantenere piccoli i metodi che rendono le uscite multiple meno di un'impedenza alla comprensione. Nel seguente esempio di Kronoz copiato dall'alto, la domanda è cosa succede in // Resto del codice ... ?:

void string fooBar(string s, int? i) {

  if(string.IsNullOrEmpty(s) || i == null) return null;

  var res = someFunction(s, i);

  foreach(var r in res) {
      if(!r.Passed) return null;
  }

  // Rest of code...

  return ret;
}

Mi rendo conto che l'esempio è in qualche modo inventato, ma sarei tentato di refactificare il ciclo foreach in un'istruzione LINQ che potrebbe quindi essere considerata una clausola di guardia. Ancora una volta, in un esempio forzato l'intento del codice non è apparente e someFunction () può avere qualche altro effetto collaterale o il risultato può essere utilizzato nella // Resto del codice ... .

if (string.IsNullOrEmpty(s) || i == null) return null;
if (someFunction(s, i).Any(r => !r.Passed)) return null;

Dando la seguente funzione refactored:

void string fooBar(string s, int? i) {

  if (string.IsNullOrEmpty(s) || i == null) return null;
  if (someFunction(s, i).Any(r => !r.Passed)) return null;

  // Rest of code...

  return ret;
}

Il C ++ non ha eccezioni? Allora perché dovresti tornare nullinvece di lanciare un'eccezione che indica che l'argomento non è accettato?
Maarten Bodewes,

1
Come ho indicato, il codice di esempio viene copiato da una risposta precedente ( stackoverflow.com/a/36729/132599 ). L'esempio originale ha restituito valori nulli e refactoring per generare eccezioni di argomenti non rilevanti al punto che stavo cercando di fare o alla domanda originale. Per buona pratica, quindi sì, normalmente (in C #) getterei ArgumentNullException in una clausola di guardia piuttosto che restituire un valore nullo.
David Clarke,

7

Un buon motivo che mi viene in mente è per la manutenzione del codice: hai un unico punto di uscita. Se vuoi cambiare il formato del risultato, ..., è solo molto più semplice da implementare. Inoltre, per il debug, puoi semplicemente attaccare un punto di interruzione lì :)

Detto questo, una volta ho dovuto lavorare in una biblioteca in cui gli standard di codifica imponevano "una dichiarazione di ritorno per funzione", e l'ho trovato piuttosto difficile. Scrivo un sacco di codice di calcolo numerico e spesso ci sono "casi speciali", quindi il codice ha finito per essere piuttosto difficile da seguire ...


Questo non fa davvero la differenza. Se si modifica il tipo della variabile locale che viene restituita, sarà necessario correggere tutte le assegnazioni a quella variabile locale. Ed è probabilmente meglio definire comunque un metodo con una firma diversa, poiché dovrete correggere anche tutte le chiamate al metodo.
Maarten Bodewes,

@ MaartenBodewes-owlstead - Può fare la differenza. Per citarne solo due esempi in cui si desidera non avere per risolvere tutte le assegnazioni per la variabile locale o modificare le chiamate di metodo, la funzione potrebbe restituire una data come una stringa (la variabile locale sarebbe una data effettiva, formattato come una stringa solo l'ultimo momento), oppure potrebbe restituire un numero decimale e si desidera modificare il numero di posizioni decimali.
nnnnnn,

@nnnnnn OK, se vuoi fare la post elaborazione sull'output ... Ma vorrei solo generare un nuovo metodo che fa la post elaborazione e lasciare solo quello vecchio. Questo è solo leggermente più difficile da refactoring, ma dovrai comunque verificare se le altre chiamate sono compatibili con il nuovo formato. Ma è comunque un motivo valido.
Maarten Bodewes,

7

I punti di uscita multipli vanno bene per funzioni abbastanza piccole, ovvero una funzione che può essere visualizzata su una lunghezza dello schermo nella sua interezza. Se una funzione lunga include anche più punti di uscita, è un segno che la funzione può essere ulteriormente suddivisa.

Detto questo, evito le funzioni di uscita multipla a meno che non sia assolutamente necessario . Ho sentito dolore per i bug dovuti a qualche randagio in qualche linea oscura in funzioni più complesse.


6

Ho lavorato con terribili standard di codifica che hanno imposto un unico percorso di uscita su di te e il risultato è quasi sempre spaghetti non strutturati se la funzione è tutt'altro che banale: finisci con molte pause e continua che ti si mettono di mezzo.


Per non parlare del dover dire alla tua mente di saltare l' ifaffermazione di fronte a ogni chiamata di metodo che restituisce successo o no :(
Maarten Bodewes,

6

Un singolo punto di uscita - tutte le altre cose uguali - rende il codice significativamente più leggibile. Ma c'è un problema: la costruzione popolare

resulttype res;
if if if...
return res;

è un falso, "res =" non è molto meglio di "return". Ha una singola istruzione return, ma più punti in cui la funzione termina effettivamente.

Se hai una funzione con più ritorni (o "res =" s), è spesso una buona idea suddividerla in diverse funzioni più piccole con un singolo punto di uscita.


6

La mia solita politica è di avere una sola dichiarazione di ritorno alla fine di una funzione a meno che la complessità del codice non sia notevolmente ridotta aggiungendone altre. In realtà, sono piuttosto un fan di Eiffel, che impone l'unica regola di ritorno non avendo alcuna dichiarazione di ritorno (c'è solo una variabile 'risultato' creata automaticamente per inserire il tuo risultato).

Ci sono certamente casi in cui il codice può essere reso più chiaro con più ritorni rispetto alla versione ovvia senza di essi. Si potrebbe sostenere che sono necessarie ulteriori rilavorazioni se si dispone di una funzione troppo complessa per essere comprensibile senza dichiarazioni di ritorno multiple, ma a volte è bene essere pragmatici su tali cose.


5

Se si ottengono più di alcuni ritorni, potrebbe esserci qualcosa di sbagliato nel codice. Altrimenti concordo sul fatto che a volte è bello poter tornare da più punti in una subroutine, specialmente quando rende il codice più pulito.

Perl 6: cattivo esempio

sub Int_to_String( Int i ){
  given( i ){
    when 0 { return "zero" }
    when 1 { return "one" }
    when 2 { return "two" }
    when 3 { return "three" }
    when 4 { return "four" }
    ...
    default { return undef }
  }
}

sarebbe meglio scrivere così

Perl 6: buon esempio

@Int_to_String = qw{
  zero
  one
  two
  three
  four
  ...
}
sub Int_to_String( Int i ){
  return undef if i < 0;
  return undef unless i < @Int_to_String.length;
  return @Int_to_String[i]
}

Nota che questo è stato solo un rapido esempio


Ok Perché questo è stato votato? Non è che non sia un'opinione.
Brad Gilbert,

5

Voto per ritorno singolo alla fine come linea guida. Questo aiuta una comune gestione della pulizia del codice ... Ad esempio, dai un'occhiata al seguente codice ...

void ProcessMyFile (char *szFileName)
{
   FILE *fp = NULL;
   char *pbyBuffer = NULL:

   do {

      fp = fopen (szFileName, "r");

      if (NULL == fp) {

         break;
      }

      pbyBuffer = malloc (__SOME__SIZE___);

      if (NULL == pbyBuffer) {

         break;
      }

      /*** Do some processing with file ***/

   } while (0);

   if (pbyBuffer) {

      free (pbyBuffer);
   }

   if (fp) {

      fclose (fp);
   }
}

Si vota per ritorno singolo - in codice C. Ma cosa succede se si codifica in una lingua con Garbage Collection e si tenta ... definitivamente blocchi?
Anthony,

4

Questa è probabilmente una prospettiva insolita, ma penso che chiunque creda che debbano essere preferite le dichiarazioni di ritorno multiple non ha mai dovuto usare un debugger su un microprocessore che supporta solo 4 breakpoint hardware. ;-)

Mentre i problemi del "codice freccia" sono completamente corretti, un problema che sembra scomparire quando si utilizzano più istruzioni return è nella situazione in cui si utilizza un debugger. Non hai una comoda posizione generale per mettere un breakpoint per garantire che vedrai l'uscita e quindi le condizioni di ritorno.


5
È solo un diverso tipo di ottimizzazione prematura. Non dovresti mai ottimizzare per il caso speciale. Se ti ritrovi a eseguire il debug di una specifica sezione di codice, c'è molto di più sbagliato rispetto al numero di punti di uscita che ha.
Wedge

Dipende anche dal tuo debugger.
Maarten Bodewes,

4

Più dichiarazioni di ritorno hai in una funzione, maggiore è la complessità in quell'unico metodo. Se ti stai chiedendo se hai troppe dichiarazioni di ritorno, potresti chiederti se hai troppe righe di codice in quella funzione.

Ma no, non c'è niente di sbagliato in una / molte dichiarazioni di ritorno. In alcune lingue, è una pratica migliore (C ++) rispetto ad altre (C).

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.