Da dove viene la nozione di "un solo ritorno"?

1004

Parlo spesso con programmatori che dicono " Non inserire più dichiarazioni di reso nello stesso metodo. " Quando chiedo loro di dirmi i motivi per cui, tutto ciò che ottengo è " Lo standard di codifica dice così. "o" È confuso. "Quando mi mostrano le soluzioni con una sola dichiarazione di ritorno, il codice mi sembra più brutto. Ad esempio:

if (condition)
   return 42;
else
   return 97;

" Questo è brutto, devi usare una variabile locale! "

int result;
if (condition)
   result = 42;
else
   result = 97;
return result;

In che modo questo codice del 50% gonfia rende il programma più facile da capire? Personalmente, lo trovo più difficile, perché lo spazio degli stati è appena aumentato da un'altra variabile che potrebbe facilmente essere prevenuta.

Ovviamente, normalmente scriverei:

return (condition) ? 42 : 97;

Ma molti programmatori rifiutano l'operatore condizionale e preferiscono il formato lungo.

Da dove viene questa nozione di "un solo ritorno"? C'è una ragione storica per cui è nata questa convenzione?

    
posta fredoverflow 06.07.2017 - 23:54
fonte

9 risposte

1053

"Single Entry, Single Exit" è stato scritto quando la maggior parte della programmazione è stata eseguita in linguaggio assembly, FORTRAN o COBOL. È stato ampiamente frainteso, perché le lingue moderne non supportano le pratiche che Dijkstra stava mettendo in guardia contro.

"Single Entry" significava "non creare punti di ingresso alternativi per le funzioni". Nel linguaggio assembly, ovviamente, è possibile inserire una funzione in qualsiasi istruzione. FORTRAN supportava più voci per le funzioni con l'istruzione ENTRY :

      SUBROUTINE S(X, Y)
      R = SQRT(X*X + Y*Y)
C ALTERNATE ENTRY USED WHEN R IS ALREADY KNOWN
      ENTRY S2(R)
      ...
      RETURN
      END

C USAGE
      CALL S(3,4)
C ALTERNATE USAGE
      CALL S2(5)

"Uscita singola" significava che una funzione dovrebbe restituire a solo una posizione: l'istruzione immediatamente successiva alla chiamata. È stato non che una funzione dovrebbe restituire da solo una posizione. Quando è stata scritta Programmazione strutturata , era prassi comune che una funzione indicasse un errore tornando in una posizione alternativa. FORTRAN supportato questo tramite "ritorno alternativo":

C SUBROUTINE WITH ALTERNATE RETURN.  THE '*' IS A PLACE HOLDER FOR THE ERROR RETURN
      SUBROUTINE QSOLVE(A, B, C, X1, X2, *)
      DISCR = B*B - 4*A*C
C NO SOLUTIONS, RETURN TO ERROR HANDLING LOCATION
      IF DISCR .LT. 0 RETURN 1
      SD = SQRT(DISCR)
      DENOM = 2*A
      X1 = (-B + SD) / DENOM
      X2 = (-B - SD) / DENOM
      RETURN
      END

C USE OF ALTERNATE RETURN
      CALL QSOLVE(1, 0, 1, X1, X2, *99)
C SOLUTION FOUND
      ...
C QSOLVE RETURNS HERE IF NO SOLUTIONS
99    PRINT 'NO SOLUTIONS'

Entrambe queste tecniche erano altamente soggette a errori. L'uso di voci alternative spesso lasciava alcune variabili non inizializzate. L'uso di ritorni alternativi aveva tutti i problemi di un'istruzione GOTO, con l'ulteriore complicazione che la condizione del ramo non era adiacente al ramo, ma da qualche parte nella subroutine.

    
risposta data 14.07.2017 - 04:48
fonte
891

Questa nozione di Single Entry, Single Exit (SESE) deriva da lingue con gestione delle risorse esplicite , come C e assembly. In C, codice come questo perderà risorse:

void f()
{
  resource res = acquire_resource();  // think malloc()
  if( f1(res) )
    return; // leaks res
  f2(res);
  release_resource(res);  // think free()
}

In tali lingue, in pratica hai tre opzioni:

  • Replica il codice di pulizia.
    Ugh. La ridondanza è sempre cattiva.

  • Utilizza un goto per passare al codice cleanup.
    Ciò richiede che il codice di pulizia sia l'ultima cosa nella funzione. (E questo è il motivo per cui alcuni sostengono che goto ha il suo posto ed ha effettivamente - in C.)

  • Introdurre una variabile locale e manipolare il flusso di controllo attraverso questo.
    Lo svantaggio è che il flusso di controllo manipolato tramite sintassi (pensa break , return , if , while ) è molto più facile da seguire rispetto al flusso di controllo manipolato attraverso lo stato delle variabili (perché quelle variabili non hanno stato quando tu guarda all'algoritmo).

In assembly è anche più strano, perché puoi saltare a qualsiasi indirizzo in una funzione quando chiami quella funzione, il che significa in pratica avere un numero quasi illimitato di punti di ingresso per qualsiasi funzione. (Talvolta questo è utile. Questi thunk sono una tecnica comune per i compilatori per implementare la regolazione del puntatore this necessaria per chiamare le funzioni virtual negli scenari di ereditarietà multipla in C ++.)

Quando devi gestire le risorse manualmente, sfruttare le opzioni per entrare o uscire da una funzione ovunque porta a un codice più complesso, e quindi a bug. Pertanto, apparve una scuola di pensiero che propagava SESE, al fine di ottenere un codice più pulito e meno bug.

Tuttavia, quando una lingua presenta delle eccezioni, (quasi) qualsiasi funzione potrebbe essere abbandonata prematuramente a (quasi) qualsiasi punto, quindi è necessario fare comunque delle disposizioni per un ritorno prematuro. (Penso che finally sia usato principalmente per quello in Java e using (quando si implementa IDisposable , finally altrimenti) in C #; C ++ impiega invece RAII .) Una volta fatto ciò, non puoi non riuscire a ripulire dopo te stesso a causa di una precoce dichiarazione return , quindi quello che è probabilmente l'argomento più strong in il favore di SESE è svanito.

Questo lascia la leggibilità. Naturalmente, una funzione di 200 LoC con una mezza dozzina di% di dichiarazioni di co_de sparse casualmente su di essa non è un buon stile di programmazione e non rende il codice leggibile. Ma una tale funzione non sarebbe facile da capire senza quei ritorni prematuri.

Nelle lingue in cui le risorse non sono o non dovrebbero essere gestite manualmente, c'è poco o nessun valore nell'aderire alla vecchia convenzione SESE. OTOH, come ho sostenuto sopra, SESE spesso rende il codice più complesso . È un dinosauro che (ad eccezione di C) non si adatta bene alla maggior parte delle lingue odierne. Invece di aiutare la comprensibilità del codice, lo ostacola.

Perché i programmatori Java si limitano a questo? Non lo so, ma dal mio (fuori) POV, Java ha preso molte convenzioni da C (dove hanno senso) e le ha applicate al suo mondo OO (dove sono inutili o addirittura brutte), dove ora si attacca a loro, non importa quali siano i costi. (Come la convenzione per definire tutte le variabili all'inizio dello scope.)

I programmatori si attaccano a tutti i tipi di notazioni strane per ragioni irrazionali. (Affermazioni strutturali profondamente annidate - "punte di freccia" - erano, in lingue come Pascal, una volta viste come codice bello.) Applicare un puro ragionamento logico a questo sembra non riuscire a convincere la maggioranza di loro a deviare dai loro modi stabiliti. Il modo migliore per cambiare tali abitudini è probabilmente quello di insegnare loro presto a fare ciò che è meglio, non ciò che è convenzionale. Tu, essendo un insegnante di programmazione, ce l'hai in mano. return

    
risposta data 07.07.2017 - 01:12
fonte
80

Da un lato, le dichiarazioni di restituzione semplici rendono la registrazione più semplice, così come forme di debug che si basano sulla registrazione. Ricordo molte volte che dovevo ridurre la funzione in un singolo ritorno solo per stampare il valore di ritorno in un singolo punto.

  int function() {
     if (bidi) { print("return 1"); return 1; }
     for (int i = 0; i < n; i++) {
       if (vidi) { print("return 2"); return 2;}
     }
     print("return 3");
     return 3;
  }

D'altra parte, puoi refactoring in function() che chiama _function() e registra il risultato.

    
risposta data 06.07.2017 - 23:57
fonte
51

"Single Entry, Single Exit" è nato con la rivoluzione della programmazione strutturata dei primi anni '70, che è stata avviata dalla lettera di Edsger W. Dijkstra all'editore " GOTO Statement Considerated Harmful ". I concetti alla base della programmazione strutturata sono stati descritti dettagliatamente nel libro classico "Structured Programming" di Ole Johan-Dahl, Edsger W. Dijkstra e Charles Anthony Richard Hoare.

"GOTO Statement Considerated Harmful" è richiesta la lettura, anche oggi. "Programmazione strutturata" è datata, ma ancora molto, molto gratificante, e dovrebbe essere in cima alla lista "Da leggere" di qualsiasi sviluppatore, molto al di sopra di qualsiasi cosa, ad es. Steve McConnell. (La sezione di Dahl illustra le basi delle classi in Simula 67, che sono la base tecnica per le classi in C ++ e tutta la programmazione orientata agli oggetti.)

    
risposta data 09.11.2011 - 15:22
fonte
31

È sempre facile collegare Fowler.

Uno degli esempi principali che vanno contro SESE sono le clausole di salvaguardia:

Sostituisci condizionale nidificato con clausole di guardia

Use Guard Clauses for all the special cases

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

                                                                                                         http://www.refactoring.com/catalog/arrow.gif

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

For more information see page 250 of Refactoring...

    
risposta data 06.07.2017 - 23:58
fonte
10

Ho scritto un post sul blog su questo argomento qualche tempo fa.

La linea di fondo è che questa regola deriva dall'età delle lingue che non dispongono della garbage collection o della gestione delle eccezioni. Non esiste uno studio formale che dimostri che questa regola porti a un codice migliore nelle lingue moderne. Sentiti libero di ignorarlo ogni volta che questo porterà a un codice più breve o più leggibile. I ragazzi di Java che insistono su questo sono ciecamente e incondizionati seguendo una regola obsoleta e inutile.

Questa domanda è stata anche posta su Stackoverflow

    
risposta data 23.05.2017 - 14:40
fonte
6

Un ritorno rende più facile il refactoring. Prova ad eseguire "metodo di estrazione" sul corpo interno di un ciclo for che contiene un ritorno, un'interruzione o una continuazione. Questo fallirà quando avrai interrotto il tuo flusso di controllo.

Il punto è: immagino che nessuno stia fingendo di scrivere un codice perfetto. Quindi il codice è regolarmente sottoposto a refactoring per essere "migliorato" ed esteso. Quindi il mio obiettivo sarebbe quello di mantenere il mio codice come refactoring amichevole possibile.

Spesso affronto il problema che devo riformulare completamente le funzioni se contengono interruttori di flusso di controllo e se voglio aggiungere solo poche funzionalità. Questo è molto incline all'errore quando si cambia tutto il flusso di controllo invece di introdurre nuovi percorsi per nidificazione isolata. Se hai un solo ritorno alla fine o se usi le guardie per uscire da un loop, hai ovviamente più nidificazione e più codice. Ma ottieni il compilatore e IDE supportate capacità di refactoring.

    
risposta data 16.10.2017 - 21:03
fonte
5

Considera il fatto che più dichiarazioni di reso equivalgono ad avere GOTO in una singola dichiarazione di reso. Questo è lo stesso caso con le dichiarazioni di rottura. Così, alcuni, come me, li considerano GOTO a tutti gli effetti.

Tuttavia, non considero dannosi questi tipi di GOTO e non esiterò a utilizzare un GOTO effettivo nel mio codice se trovo una buona ragione per questo.

La mia regola generale è che GOTO è solo per il controllo del flusso. Non dovrebbero mai essere usati per il looping e non si dovrebbe mai GOTO 'upwards' o 'backwards'. (che è il modo in cui funzionano le interruzioni / i ritorni)

Come altri hanno già menzionato, è necessario leggere quanto segue Dichiarazione GOTO considerata dannosa
Tuttavia, tieni presente che questo è stato scritto nel 1970 quando GOTO era abusato. Non tutti i GOTO sono dannosi e non sconsigliamo il loro uso se non li utilizzi al posto dei normali costrutti, ma piuttosto nel caso strano che l'utilizzo di costrutti normali sarebbe estremamente scomodo.

Trovo che utilizzarli nei casi di errore in cui è necessario sfuggire a un'area a causa di un errore che non dovrebbe mai verificarsi in casi normali utile a volte. Ma dovresti anche prendere in considerazione la possibilità di inserire questo codice in una funzione separata in modo che tu possa tornare presto invece di usare GOTO ... ma a volte è anche scomodo.

    
risposta data 10.11.2011 - 17:31
fonte
1

Complessità ciclomatica

Ho visto che SonarCube utilizza una dichiarazione di ritorno multipla per determinare la complessità ciclomatica. Quindi più le dichiarazioni di ritorno, maggiore è la complessità ciclomatica

Cambia tipo di ritorno

Rendimenti multipli significa che dobbiamo cambiare in più punti della funzione quando decidiamo di cambiare il nostro tipo di ritorno.

Uscita multipla

È più difficile eseguire il debug poiché la logica deve essere attentamente studiata insieme alle istruzioni condizionali per capire che cosa ha causato il valore restituito.

Soluzione refactored

La soluzione a più dichiarazioni di reso è quella di sostituirle con il polimorfismo con un singolo ritorno dopo aver risolto l'oggetto di implementazione richiesto.

    
risposta data 08.10.2018 - 13:33
fonte