Perché una funzione dovrebbe avere un solo punto di uscita? [chiuso]


97

Ho sempre sentito parlare di una singola funzione di punto di uscita come un cattivo modo di codificare perché perdi leggibilità ed efficienza. Non ho mai sentito nessuno discutere dall'altra parte.

Pensavo avesse qualcosa a che fare con CS, ma questa domanda è stata risolta in cstheory stackexchange.



6
La risposta è che non esiste una risposta che sia sempre giusta. Spesso trovo più facile codificare con più uscite. Ho anche scoperto (durante l'aggiornamento del codice sopra) che la modifica / estensione del codice era più difficile a causa di quelle stesse uscite multiple. Prendere queste decisioni caso per caso è il nostro lavoro. Quando una decisione ha sempre una risposta "migliore", non ce n'è bisogno.
JS.

1
@finnw i mod fascisti hanno rimosso le ultime due domande, per assicurarsi che debbano ricevere una risposta ancora, e ancora, e ancora
Maarten Bodewes,

Nonostante la parola "discutere" nella domanda, non credo proprio che questa sia una domanda basata sull'opinione. È abbastanza rilevante per un buon design, ecc. Non vedo motivo per chiuderlo, ma w / e.
Ungeheuer

1
Un unico punto di uscita semplifica il debug, la lettura, la misurazione e l'ottimizzazione delle prestazioni, il refactoring. Ciò è oggettivo e materialmente significativo. L'uso dei primi ritorni (dopo semplici controlli degli argomenti) crea una miscela sensata di entrambi gli stili. Dati i vantaggi di un unico punto di uscita, sporcare il codice con i valori di ritorno è semplicemente la prova di un programmatore pigro, sciatto e incurante - e almeno possibilmente non gradito i cuccioli.
Rick O'Shea

Risposte:


108

Esistono diverse scuole di pensiero e dipende in gran parte dalle preferenze personali.

Uno è che crea meno confusione se c'è un solo punto di uscita: hai un unico percorso attraverso il metodo e sai dove cercare l'uscita. Sul lato negativo, se usi il rientro per rappresentare l'annidamento, il tuo codice finisce per rientrare in modo massiccio a destra e diventa molto difficile seguire tutti gli ambiti annidati.

Un altro è che puoi controllare le precondizioni ed uscire in anticipo all'inizio di un metodo, in modo da sapere nel corpo del metodo che determinate condizioni sono vere, senza che l'intero corpo del metodo sia rientrato di 5 miglia a destra. Questo di solito riduce al minimo il numero di ambiti di cui ti devi preoccupare, il che rende il codice molto più facile da seguire.

Un terzo è che puoi uscire ovunque tu voglia. Un tempo questo creava più confusione, ma ora che abbiamo editor e compilatori che colorano la sintassi che rilevano il codice non raggiungibile, è molto più facile da gestire.

Sono esattamente nel campo centrale. L'imposizione di un singolo punto di uscita è una restrizione inutile o addirittura controproducente IMHO, mentre uscire a caso su un metodo a volte può portare a una logica disordinata e difficile da seguire, dove diventa difficile vedere se un dato bit di codice sarà o non sarà eseguito. Ma il "gating" del metodo consente di semplificare notevolmente il corpo del metodo.


1
La nidificazione profonda può essere ovviata nel singe exitparadigma a forza di go toaffermazioni. Inoltre, si ha la possibilità di eseguire un po 'di postelaborazione sotto l' Erroretichetta locale della funzione , cosa impossibile con più returns.
Ant_222

2
Di solito c'è una buona soluzione che evita la necessità di andare a. Preferisco di gran lunga "restituire (Fail (...))" e inserire il codice di pulizia condiviso nel metodo Fail. Ciò potrebbe richiedere il passaggio di alcuni locali per consentire il rilascio della memoria, ecc., Ma a meno che non ti trovi in ​​un bit di codice critico per le prestazioni, questa di solito è una soluzione molto più pulita di un IMO goto. Consente inoltre a diversi metodi di condividere anche un codice di pulizia simile.
Jason Williams

Esiste un approccio ottimale basato su criteri oggettivi, ma possiamo essere d'accordo sul fatto che esistono scuole di pensiero (corrette e scorrette) e che si riducono alle preferenze personali (una preferenza a favore o contro un approccio corretto).
Rick O'Shea

39

La mia raccomandazione generale è che le istruzioni di ritorno dovrebbero, quando possibile, essere posizionate prima del primo codice che ha effetti collaterali o dopo l'ultimo codice che ha effetti collaterali. Considererei qualcosa di simile:

  if (! argomento) // Controlla se non è nullo
    return ERR_NULL_ARGUMENT;
  ... processare un argomento non nullo
  se (ok)
    return 0;
  altro
    return ERR_NOT_OK;

più chiaro di:

  int valore_ritorno;
  if (argomento) // Non nullo
  {
    .. processare un argomento non nullo
    .. impostare il risultato in modo appropriato
  }
  altro
    risultato = ERR_NULL_ARGUMENT;
  risultato di ritorno;

Se una certa condizione dovrebbe impedire a una funzione di fare qualcosa, preferisco uscire anticipatamente dalla funzione in un punto sopra il punto in cui la funzione farebbe qualsiasi cosa. Una volta che la funzione ha intrapreso azioni con effetti collaterali, però, preferisco tornare dal basso, per chiarire che tutti gli effetti collaterali devono essere affrontati.


Il tuo primo esempio, la gestione della okvariabile, mi sembra l'approccio a ritorno singolo. Inoltre, il blocco if-else può essere semplicemente riscritto in:return ok ? 0 : ERR_NOT_OK;
Melebius

2
Il primo esempio ha un returnall'inizio prima di tutto il codice che fa tutto. Per quanto riguarda l'utilizzo ?:dell'operatore, scriverlo su righe separate rende più facile per molti IDE allegare un punto di interruzione di debug allo scenario non ok. A proposito, la vera chiave del "singolo punto di uscita" sta nel capire che ciò che conta è che per ogni chiamata particolare a una funzione normale, il punto di uscita è il punto immediatamente dopo la chiamata . I programmatori oggigiorno lo danno per scontato, ma le cose non sono sempre state così. In alcuni rari casi il codice potrebbe dover cavarsela senza spazio nello stack, portando a funzioni ...
supercat

... che escono tramite gotos condizionali o calcolati. Generalmente qualsiasi cosa con risorse sufficienti per essere programmata in qualcosa di diverso dal linguaggio assembly sarà in grado di supportare uno stack, ma ho scritto codice assembly che doveva funzionare sotto alcuni vincoli molto stretti (fino a ZERO byte di RAM in un caso), e avere più punti di uscita può essere utile in questi casi.
supercat

1
Il cosiddetto esempio più chiaro è molto meno chiaro e difficile da leggere. Un punto di uscita è sempre più facile da leggere, più facile da mantenere, più facile da eseguire il debug.
GuidoG

8
@GuidoG: entrambi i pattern potrebbero essere più leggibili, a seconda di ciò che appare nelle sezioni omesse. Utilizzando "return x;" rende chiaro che se l'istruzione viene raggiunta, il valore restituito sarà x. Utilizzando "risultato = x;" lascia aperta la possibilità che qualcos'altro possa modificare il risultato prima che venga restituito. Ciò può essere utile se in effetti diventasse necessario modificare il risultato, ma i programmatori che esaminano il codice dovrebbero ispezionarlo per vedere come il risultato potrebbe cambiare anche se la risposta è "non può".
supercat

15

Un unico punto di ingresso e di uscita era il concetto originale di programmazione strutturata vs Spaghetti Coding passo dopo passo. C'è la convinzione che più funzioni di punto di uscita richiedano più codice poiché è necessario pulire adeguatamente gli spazi di memoria allocati per le variabili. Si consideri uno scenario in cui la funzione alloca variabili (risorse) e uscire dalla funzione in anticipo e senza un'adeguata pulizia comporterebbe perdite di risorse. Inoltre, la creazione di clean-up prima di ogni uscita creerebbe molto codice ridondante.


Questo non è davvero un problema con RAII
BigSandwich

14

Con quasi tutto, si riduce alle esigenze del deliverable. Nei "vecchi tempi", il codice spaghetti con più punti di ritorno invitava a perdite di memoria, poiché i programmatori che preferivano quel metodo in genere non pulivano bene. Ci sono stati anche problemi con alcuni compilatori che "perdevano" il riferimento alla variabile return quando lo stack veniva estratto durante il ritorno, nel caso di ritorno da uno scope annidato. Il problema più generale era quello del codice rientrante, che tenta di fare in modo che lo stato di chiamata di una funzione sia esattamente lo stesso del suo stato di ritorno. I mutatori di oop hanno violato questo e il concetto è stato accantonato.

Ci sono risultati finali, in particolare i kernel, che richiedono la velocità fornita da più punti di uscita. Questi ambienti normalmente hanno una propria memoria e gestione dei processi, quindi il rischio di una perdita è ridotto al minimo.

Personalmente, mi piace avere un unico punto di uscita, dal momento che lo uso spesso per inserire un punto di interruzione sull'istruzione return ed eseguire un'ispezione del codice su come il codice ha determinato quella soluzione. Potrei semplicemente andare all'ingresso e attraversarlo, cosa che faccio con soluzioni ampiamente nidificate e ricorsive. In qualità di revisore del codice, più resi in una funzione richiedono un'analisi molto più approfondita, quindi se lo fai per accelerare l'implementazione, stai derubando Peter per salvare Paul. Sarà richiesto più tempo nelle revisioni del codice, invalidando la presunzione di un'implementazione efficiente.

- 2 centesimi

Si prega di consultare questo documento per maggiori dettagli: NISTIR 5459


8
multiple returns in a function requires a much deeper analysissolo se la funzione è già enorme (> 1 schermata), altrimenti facilita l'analisi
dss539

2
i ritorni multipli non rendono mai l'analisi più facile, solo l'opposto
GuidoG

1
il collegamento è morto (404).
fusi

1
@fusi - l'ho trovato su archive.org e ho aggiornato il link qui
sscheider

4

A mio avviso, il consiglio di uscire da una funzione (o da un'altra struttura di controllo) in un solo punto è spesso ipervenduto. In genere vengono fornite due ragioni per uscire in un solo punto:

  1. Il codice a uscita singola è presumibilmente più facile da leggere ed eseguire il debug. (Ammetto che non penso molto a questo motivo, ma è dato. Ciò che è sostanzialmente più facile da leggere ed eseguire il debug è il codice a ingresso singolo .)
  2. Il codice a uscita singola si collega e restituisce in modo più pulito.

La seconda ragione è sottile e ha qualche merito, soprattutto se la funzione restituisce una grande struttura di dati. Tuttavia, non me ne preoccuperei troppo, tranne ...

Se sei uno studente, vuoi ottenere il massimo dei voti nella tua classe. Fai quello che preferisce l'istruttore. Probabilmente ha una buona ragione dal suo punto di vista; quindi, per lo meno, imparerai la sua prospettiva. Questo ha valore in sé.

In bocca al lupo.


4

Ero un sostenitore dello stile a uscita singola. Il mio ragionamento proveniva principalmente dal dolore ...

L'uscita singola è più facile da eseguire il debug.

Date le tecniche e gli strumenti che abbiamo oggi, questa è una posizione molto meno ragionevole da prendere in quanto i test unitari e la registrazione possono rendere superflua l'uscita singola. Detto questo, quando è necessario osservare l'esecuzione del codice in un debugger, è stato molto più difficile capire e lavorare con codice contenente più punti di uscita.

Ciò è diventato particolarmente vero quando era necessario intercettare assegnazioni per esaminare lo stato (sostituito con espressioni di controllo nei debugger moderni). Era anche troppo facile alterare il flusso di controllo in modi che nascondevano il problema o interrompevano del tutto l'esecuzione.

I metodi a uscita singola erano più facili da eseguire nel debugger e più facili da separare senza rompere la logica.


0

La risposta dipende molto dal contesto. Se stai creando una GUI e hai una funzione che inizializza le API e apre le finestre all'inizio del tuo main, sarà pieno di chiamate che potrebbero generare errori, ognuno dei quali causerebbe la chiusura dell'istanza del programma. Se hai usato istruzioni IF annidate e indentavi, il tuo codice potrebbe rapidamente diventare molto inclinato a destra. Restituire un errore in ogni fase potrebbe essere migliore e in realtà più leggibile pur essendo altrettanto facile da eseguire il debug con alcuni flag nel codice.

Se, tuttavia, stai testando condizioni diverse e restituisci valori diversi a seconda dei risultati nel tuo metodo, potrebbe essere una pratica molto migliore avere un unico punto di uscita. Lavoravo sugli script di elaborazione delle immagini in MATLAB che potevano diventare molto grandi. Più punti di uscita potrebbero rendere il codice estremamente difficile da seguire. Le dichiarazioni di switch erano molto più appropriate.

La cosa migliore da fare sarebbe imparare mentre procedi. Se stai scrivendo codice per qualcosa, prova a trovare il codice di altre persone e vedi come lo implementano. Decidi quali bit ti piacciono e quali no.


-6

Se ritieni di aver bisogno di più punti di uscita in una funzione, la funzione è troppo grande e sta facendo troppo.

Consiglierei di leggere il capitolo sulle funzioni nel libro di Robert C. Martin, Clean Code.

In sostanza, dovresti provare a scrivere funzioni con 4 righe di codice o meno.

Alcune note dal blog di Mike Long :

  • La prima regola delle funzioni: dovrebbero essere piccole
  • La seconda regola delle funzioni: dovrebbero essere più piccole di quella
  • I blocchi all'interno di istruzioni if, istruzioni while, cicli for, ecc. Dovrebbero essere lunghi una riga
  • ... e quella riga di codice sarà solitamente una chiamata di funzione
  • Non dovrebbero esserci più di uno o forse due livelli di rientro
  • Le funzioni dovrebbero fare una cosa
  • Le istruzioni di funzione dovrebbero essere tutte allo stesso livello di astrazione
  • Una funzione non dovrebbe avere più di 3 argomenti
  • Gli argomenti di output sono un odore di codice
  • Passare un flag booleano in una funzione è davvero orribile. Per definizione stai facendo due cose nella funzione.
  • Gli effetti collaterali sono bugie.

29
4 linee? Che cosa codifichi che ti consente una tale semplicità? Dubito davvero che il kernel Linux o git, ad esempio, lo facciano.
shinzou

7
"Passare un flag booleano in una funzione è davvero orribile. Per definizione stai facendo due cose nella funzione." Per definizione? No ... quel booleano potrebbe potenzialmente interessare solo una delle tue quattro righe. Anche se sono d'accordo con mantenere le dimensioni della funzione ridotte, quattro è un po 'troppo restrittivo. Questo dovrebbe essere considerato come una linea guida molto approssimativa.
Jesse

12
L'aggiunta di restrizioni come queste creerà inevitabilmente confusione nel codice. Si tratta più di metodi che sono chiari e concisi e si limitano a fare solo ciò che dovrebbero fare senza effetti collaterali inutili.
Jesse

10
Una delle rare risposte su SO che vorrei poter votare più volte.
Steven Rands

8
Sfortunatamente questa risposta sta facendo più cose e probabilmente doveva essere suddivisa in più altre, tutte con meno di quattro righe.
Eli
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.