it-swarm.it

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

Parlo spesso con i programmatori che dicono "Non mettere più dichiarazioni di ritorno nello stesso metodo." Quando chiedo loro di dirmi i motivi, tutto quello che ottengo è "La codifica lo dice lo standard. "o" È confuso. "Quando mi mostrano soluzioni con una singola dichiarazione di ritorno, il codice mi sembra più brutto. Per esempio:

if (condition)
   return 42;
else
   return 97;

"È brutto, devi usare una variabile locale!"

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

In che modo questo bloat del codice del 50% semplifica la comprensione del programma? Personalmente, lo trovo più difficile, perché lo spazio degli stati è appena aumentato di un'altra variabile che avrebbe potuto essere facilmente evitato.

Certo, normalmente scriverei solo:

return (condition) ? 42 : 97;

Ma molti programmatori evitano l'operatore condizionale e preferiscono la forma lunga.

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

1077
fredoverflow

"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 contro cui Dijkstra stava mettendo in guardia.

"Voce singola" significa "non creare punti di entrata alternativi per le funzioni". Nel linguaggio Assembly, ovviamente, è possibile inserire una funzione in qualsiasi istruzione. FORTRAN ha supportato 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" significa che una funzione deve restituire solo a un posto: l'istruzione immediatamente successiva alla chiamata. no significava che una funzione dovrebbe restituire solo da un posto. Quando Programmazione strutturata è stata scritta, era pratica comune per una funzione indicare un errore tornando in una posizione alternativa. FORTRAN ha 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 fortemente soggette a errori. L'uso di voci alternative spesso lasciava alcune variabili non inizializzate. L'uso di ritorni alternativi presentava 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.

1145
kevin cline

Questa nozione di Single Entry, Single Exit (SESE) deriva da lingue con gestione esplicita delle risorse, 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, hai sostanzialmente tre opzioni:

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

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

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

In Assembly è ancora più strano, perché puoi chiamare qualsiasi indirizzo in una funzione quando chiami quella funzione, il che significa che in realtà hai un numero quasi illimitato di punti di ingresso per qualsiasi funzione. (A volte questo è utile. Tali thunk sono una tecnica comune per i compilatori per implementare la regolazione del puntatore this necessaria per chiamare le funzioni virtual in scenari di ereditarietà multipla in C++.)

Quando è necessario gestire le risorse manualmente, lo sfruttamento delle opzioni di immissione o chiusura di una funzione ovunque porta a un codice più complesso e quindi a bug. Pertanto, è apparsa una scuola di pensiero che ha propagato SESE, al fine di ottenere un codice più pulito e meno bug.


Tuttavia, quando una lingua presenta eccezioni, (quasi) qualsiasi funzione potrebbe essere abbandonata prematuramente in (quasi) qualsiasi punto, quindi è necessario prevedere comunque un ritorno prematuro. (Penso che finally sia usato principalmente per questo in Java e using (quando si implementa IDisposable, finally altrimenti) in C #; C++ invece impiega RAII .) Dopo aver fatto ciò, impossibile non riesci a ripulire dopo te stesso a una prima dichiarazione return, quindi qual è probabilmente l'argomento più forte a favore di SESE è svanito.

Ciò lascia leggibilità. Naturalmente, una funzione 200 LoC con una mezza dozzina di istruzioni return spruzzate 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 devono essere gestite manualmente, l'aderenza alla vecchia convenzione SESE ha poco o nessun valore. OTOH, come ho affermato sopra, SESE spesso rende il codice più complesso. È un dinosauro che (tranne C) non si adatta bene alla maggior parte delle lingue di oggi. Invece di aiutare la comprensibilità del codice, lo ostacola.


Perché Java si attaccano a questo? Non lo so, ma dal mio POV (esterno), Java ha preso molte convenzioni da C (dove hanno senso) e li hanno applicati al suo OO (dove sono inutili o decisamente cattivi), dove ora si attacca a loro, non importa quali siano i costi. (Come la convenzione per definire tutto le variabili all'inizio dell'ambito.)

I programmatori si attengono a tutti i tipi di strane notazioni per motivi irrazionali. (Dichiarazioni strutturali profondamente annidate - "punte di freccia" - erano, in linguaggi come Pascal, un tempo viste come un bel codice.) Applicare un ragionamento logico puro a questo sembra non riuscire a convincere la maggior parte di loro a deviare dai loro modi stabiliti. Il modo migliore per cambiare tali abitudini è probabilmente insegnare loro presto a fare ciò che è meglio, non ciò che è convenzionale. Tu, essendo un insegnante di programmazione, ce l'hai in mano. :)

921
sbi

Da un lato, le singole dichiarazioni di restituzione semplificano la registrazione, nonché le forme di debug che si basano sulla registrazione. Ricordo un sacco di volte che ho dovuto 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, è possibile rifattarlo in function() che chiama _function() e registra il risultato.

82
perreal

"Entrata singola, uscita singola" è nata con la rivoluzione della Programmazione strutturata dei primi anni '70, che è stata lanciata dalla lettera di Edsger W. Dijkstra all'editore " Dichiarazione GOTO considerata dannosa ". I concetti alla base della programmazione strutturata sono stati dettagliati nel libro classico "Programmazione strutturata" di Ole Johan-Dahl, Edsger W. Dijkstra e Charles Anthony Richard Hoare.

È necessario leggere "Dichiarazione GOTO considerata dannosa" anche oggi. La "Programmazione strutturata" è datata, ma comunque molto, molto gratificante e dovrebbe essere in cima all'elenco "Da leggere" di ogni sviluppatore, molto al di sopra di qualsiasi cosa, ad es. Steve McConnell. (La sezione di Dahl espone le basi delle lezioni in Simula 67, che sono le basi tecniche per le lezioni in C++ e tutta la programmazione orientata agli oggetti.)

53
John R. Strohm

È sempre facile collegare Fowler.

Uno dei principali esempi che vanno contro SESE sono le clausole di guardia:

Sostituisci condizionale nidificato con clausole di guardia

Usa le clausole di guardia per tutti i casi speciali

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();
};  

Per ulteriori informazioni, vedere a pagina 250 di Refactoring ...

36
Pieter B

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 hanno la garbage collection o la gestione delle eccezioni. Non esiste uno studio formale che dimostri che questa regola porta 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 Java ragazzi che insistono su questo sono ciecamente e senza dubbio a seguito di una regola obsoleta e inutile.

Questa domanda è stata posta anche su StackOverflow

11
Anthony

Un ritorno semplifica il refactoring. Prova a eseguire il "metodo extract" nel corpo interno di un ciclo for che contiene un ritorno, interrompi o continua. Ciò fallirà poiché hai interrotto il flusso di controllo.

Il punto è: suppongo che nessuno stia fingendo di scrivere un codice perfetto. Quindi il codice è regolarmente sotto refactoring per essere "migliorato" ed esteso. Quindi il mio obiettivo sarebbe quello di mantenere il mio codice il più amichevole possibile per il refactoring.

Spesso incontro il problema che devo riformulare completamente le funzioni se contengono interruttori di flusso di controllo e se voglio aggiungere solo poche funzionalità. Questo è molto soggetto a errori quando si modifica l'intero flusso di controllo invece di introdurre nuovi percorsi in nidificazioni isolate. Se alla fine hai un solo ritorno o se usi guardie per uscire da un ciclo, ovviamente hai più nidificazione e più codice. Ma ottieni il compilatore e IDE ha supportato le capacità di refactoring.

6
oopexpert

Considera il fatto che più dichiarazioni di reso equivalgono ad avere GOTO per una singola dichiarazione di reso. Questo è lo stesso caso con le dichiarazioni di rottura. Pertanto, 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 reale nel mio codice se trovo una buona ragione per farlo.

La mia regola generale è che i GOTO sono solo per il controllo del flusso. Non dovrebbero mai essere usati per alcun loop e non dovresti mai GOTO "verso l'alto" o "indietro". (che funziona come interruzioni/ritorni)

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

Trovo che usarli in casi di errore in cui è necessario uscire da un'area a causa di un errore che non dovrebbe mai verificarsi in casi normali sia utile a volte. Ma dovresti anche considerare di mettere questo codice in una funzione separata in modo da poter semplicemente tornare presto invece di usare un GOTO ... ma a volte è anche scomodo.

5
user606723

Complessità ciclomatica

Ho visto SonarCube utilizzare più dichiarazioni di ritorno per determinare la complessità ciclomatica. Più sono le dichiarazioni di ritorno, maggiore è la complessità ciclomatica

Modifica tipo di ritorno

I resi multipli indicano che dobbiamo cambiare in più punti della funzione quando decidiamo di cambiare il nostro tipo di reso.

Uscita multipla

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

Soluzione refactored

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

4
Sorter