Sull’early return nelle funzioni…
Qualche giorno fa stavo sistemando del codice quando ad un tratto mi sono (ri)messo a riflettere sul modo e sul “posto” più efficace ed efficiente per ritornare un valore da una funzione.
L’intera discussione viene spesso introdotta con i termini “early return” o anche “multiple return points“.
Per rendere le cose più comprensibili, introduco subito due piccole funzioni-esempio, concettualmente simili ma diverse rispetto al dove e al quando terminare, restituendo un valore di uscita.
La prima “versione” dichiara un valore “di ritorno” che verrà eventualmente modificato nel corso della funzione e verrà restituito “alla fine”.
La seconda invece ritorna il valore appena possibile (“early return” appunto).
bool DelayedReturnExample()
{
bool ret = false;
if(condition1)
ret = true;
else
{
// lots of code here...
if(condition2)
{
// lots of code here...
ret = (condition3);
}
}
return ret;
}
bool EarlyReturnExample()
{
if(condition1)
return true;
// lots of code here...
if(condition2)
{
// lots of code here...
return (condition3);
}
return true;
}
Pro
Inutile dire che le discussioni su questo tema di sprecano.
Vi rimando, ad esempio, alla domanda Is returning early from a function more elegant than an if statement? su StackOverflow.
Personalmente anche io ritengo che l’early return renda il codice più leggibile perchè rende chiara ed immediata la volontà di uscire subito e senza costringere l’eventuale “lettore” a dover scorrere tutto il codice – diramazioni degli if incluse – per capire cosa ritornerà alla fine.
Mi ritrovo quindi nella risposta di uno dei commentatori alla sopra-citata domanda:
“In most cases, returning early reduces the complexity and makes the code more readable.”
Ma tutto ha un limite…
Contro
Di contro l’early return implica che il codice può tornare in più punti (cfr. questa domanda) e ciò significa che un programmatore può trovarsi un “return” ovunque in una funzione e non solo uno alla fine.
L’abuso dell’early return, ad esempio, può rendere più complicata la fase di debug perchè poi bisogna monitorare più vie d’uscita.
Da “Top Ten Things I Gripe About In Code Reviews“:
“8. multiple return points
Up 1; we have methods with multiple return statements.
A point perhaps open to argument, but having multiple return statements in a single method make that method more difficult to read and understand. It a form of spaghetti code – and we got rid of goto for the same crime.
It can also make the code harder to debug. In an stack trace you won’t see which return statement was executed when a method ended which means that you have less of an idea what that method did and how it executed. And remember that a method might call other objects and do ’stuff’ to other ‘things’ you don’t know how much stuff happened because you don’t know what return was used.
It’s also dead-easy to write methods that only have one return statement and it doesn’t cost you anything in terms of performance. The following quote is applicable to most items in this list, it’s good here though.”
Considerazioni
Dal lato “prestazioni” non credo che seguire la strada dell’early return, allo stato attuale, ne garantisca di migliori rispetto all’altra opzione, a causa della varie trasformazioni/ottimizzazioni svolte dai moderni compilatori sul codice.
Diciamo quindi che, supponendo simili le prestazioni delle due forme, quella che fa uso dell’early return tende a rendere il codice più compatto (meno righe di codice, meno “graffe”, …) e, se vogliamo, ad agevolare il programmatore che deve rileggere il codice.
Inoltre il buon programmatore sa che le funzioni, di norma, non devono mai superare un certo numero di righe (qualcuno dice 20, altri 50, altri ancora 100, ma il succo del discorso è riassumibile in “poche righe e senza cadere nella sindrome APL“) per cui il problema tende a spostarsi dalla singola funzione all’eventuale marasma di funzioni che si chiamano fra loro.
Ne farei quindi più un discorso di gusto, stile/eleganza e manutenibità che di efficienza in senso stretto.
Sui gusti non si disputa ma l’importante è che il codice risulti il più possibile coerente e non ci siano continui cambi di metodo di ritorno fra una funzione e l’altra: la coerenza conta!
Sull’eleganza/stile, credo che sia il codice risultante a determinare la scelta.
Ci sono casi in cui l’early return riesce a rendere facile la lettura da parte del programmatore ma anche tediosa e frustrante la fase di debug a causa dei troppi breakpoint: la leggibilità è fondamentale, ma non a scapito della possibilità di debuggare, estendere e manutenere facilmente il codice!
Visione “mentale”
Però secondo me c’è dell’altro…
Tornando alle mie due piccole funzioni e tralasciando ogni considerazione a livello di prestazioni, quale delle due forme vi pare più chiara relativamente al test su condition1?
Sarà la mia visione, ma l’utilizzo accorto dell’early return mi trasmette la volontà del programmatore di uscire appena possibile, appena è certo che un risultato non cambierà: se devo leggere del codice per la prima volta e ogni if equivale a suddividere il problema in più sottoproblemi, allora l’early return mi implica chiudere subito il relativo sottoproblema, almeno a livello mentale.
Una cosa chiusa in quel modo, “brusco”, lo è definitivamente, anche nella mia testa. Per cui potenzialmente si riduce anche il numero di diramazioni, casi e sottocasi che dovrò tenere a mente mentre “esploro” il codice.
In termini “accademici”, pensando al codice e alle sue diramazioni come ad un albero (albero di decisione?), l’early return permette di arrestarmi/potare un sottoalbero istantaneamente e non costringermi ad esplorarlo (potenzialmente) tutto, giusto per capire cosa ritorna alla fine. Questo ovviamente a livello mentale.
Ovviamente se la funzione è piccola questo discorso è meramente formale, “da accademia” appunto.
In conclusione
Morale della fiaba: se/quando posso, tendo ad impiegare l’early return ma imponendomi un limite. Se la funzione è lunga al più una cinquantina di righe, al massimo ammetto 3-4 punti di uscita immediata. Metterne di più significa rischiare di complicare troppo le cose sotto ogni punto di vista…
Voi che ne pensate?

Secondo me, a meno di funzioni particolarmente semplici in cui il motivo del return e’ univocamente definito, nel caso si usi l’early return e’ sempre bene accompagnare la funzione con un codice di ritorno che indichi la motivazione per cui la funzione e’ terminata.
Tale consiglio vale anche nel caso in cui si decida di non usare l’early return, ma il debugging e’ reso piu’ semplice dall’unico punto di uscita possibile.
Dal punto di vista stilistico anche a me piace di piu’ usare l’early return anziche’ no.
Ciau Max! ^^
In effetti è quello che faccio: di solito se sfrutto l’early return per “uscire subito” metto un commento-motivazione a corredo per esplicitare la mia intenzione. Torna comoda sia in debug che in fase di rilettura del codice a distanza di tempo.
Sono d’accordo che ogni tanto applicare un return a metà della funzione possa essere più chiaro rispetto al seguire ciecamente una regola sulla qualità del codice.
.. rimane valida la considerazione sul fatto che funzioni piccole e poco complesse tendono a ridurre drasticamente dubbi di questo tipo ..
Beh, se la funzione è piccola, ogni discorso diventa relativo, quasi un voler mettere i puntini sulle i, no?
Ciau! ^^
beh c’è un taglio gordiano che si può porre sulla questione.. eliminare gli if con polimorfismo e multiple dispatch
Uhm… Sull’eliminare gli if superflui, nulla da eccepire anche se per me il multiple dispatch nei linguaggi che non lo supportano nativamente – tipo il C++ – è un overkill a livello mentale e di leggibilità.
Quanto all’argomento if stesso, direi che ormai il problema è più sulla ridotta leggibilità e manutenibilità del codice che ne abusa che non a livello di prestazioni: i compilatori odierni non sono affatto idioti e sono perfettamente in grado di riorganizzare il codice (cfr. l’assembly prodotto da codice C++ ad esempio), levando il superfluo.
Ciau e grazie per essere passato! ^^
Jp, sono capitato per caso nella versione mobile del tuo blog. Fantastica !
Ho una versione mobile? LOL ^^’
mi trovo perfettamente in linea con Jp, anche io tendo ad usare l’early return piu’ o meno per le stesse ragioni. mi sembra che in generale il risultato sia piu’ leggibile e di facile comprensione.
il gioco mi si rompe tra le mani quando all’inizio della funzione devo allocare delle risorse che comunque devo deallocare prima di uscire o comunque in caso di errore (semafori, file, gruppi di strutture parzialmente inizializzate).
a seconda del linguaggio si prospettano soluzioni differenti. ammetto di sentire la mancanza del try-catch-finally in c++, per quanto il finally non sia considerato “puro” molte volte aiuterebbe a pulire un po’ meglio.
e poi c’e’ il goto…
Ciao Cavok!
Interessante e veritiera la tua considerazione sull’inizializzazione/distruzione degli oggetti in C++ (in linguaggi come Java e C#, la garbage collection evita parte o tutto il problema).
Beh, in teoria (e dico in teoria) la mancanza del finally in C++ è risolta tramite RAII (così dicono gli esperti) e comporta l’autodistruzione degli oggetti interessati, ricorsivamente.
Da Wikipedia
Peccato però che ci siano casi in cui un’eccezione non sia poi così critica da meritare la distruzione di tutto ed un semplice finally per gestirla non sarebbe davvero male…
Un’altra soluzione efficiente in C++, garbage collection a parte (chiaramente non standard) sono gli smart-pointer ma essere costretto a usarli per emulare un finally mi pare qualcosa di discutibile…
Ciau!
aspetta un attimo, java e c# non hanno questo problema non grazie al garbage collector ma grazie al fatto che tutto e’ gia’ un oggetto.
se lavori solo con gli oggetti, il problema non si pone neanche in c++, lo stack unwinding comporta proprio la distruzione di tutti gli oggetti allocati su tutti gli stack frame tra quello che lancia l’eccezione e quello che la gestisce. il tutto seguendo le regole di scope, che tra l’altro sono statiche.
non sono ferrato in tema di garbage collector. mi pare che in java sia addirittura un thread separato, quindi non puoi contare sulla distruzione degli oggetti, che infatti non hanno distruttore. non so in c#.
Anche nel .NET Framework (non mi piace dire in C#) il Garbage Collector gira in un thread separato. Questo non esclude il fatto che il C# permetta la scrittura di distruttori, anche se nella maggior parte dei casi non servono.
il fatto che il GC giri in un thread separato implica solo che non sai quando l’oggetto verra’ distrutto, quindi non puoi contare sul distruttore.
ripensandoci, l’introduzione del finally e’ sicuramente una conseguenza diretta di questo fatto, sembra solo una comodita’ ma in realta’ se non ci fosse il linguaggio sarebbe ingestibile.
No, calma. Il mio discorso sulla GC è relativo al fatto che se sai che qualcosa verrà eliminato da una GC, l’early return lo puoi applicare serenamente senza timore di leak, nella maggior parte dei casi (cioè sai che se non chiudi qualcosa, lo farà comunque la GC per te. E spesso lo fa molto bene.).
Quanto a C# esistono due meccanismi di gestione della distruzione degli oggetti: uno implicito/automatico/non-deterministico (la GC appunto), uno esplicito/manuale/deterministico (implementando IDisposable, quindi la funzione Dispose() che ricorda un distruttore C++).
In realtà esiste anche un terzo caso, “scoped”, che fa uso dell’ennesimo significato attribuito alla keyword using.
Spero di essermi espresso bene, stavolta. ^^
brrrr…
LOL