Dovresti usare entrambi. L'importante è decidere quando usarli .
Ci sono alcuni scenari in cui le eccezioni sono la scelta ovvia :
In alcune situazioni non puoi fare nulla con il codice di errore e devi solo gestirlo in un livello superiore nello stack di chiamate , di solito basta registrare l'errore, mostrare qualcosa all'utente o chiudere il programma. In questi casi, i codici di errore richiederebbero di far apparire manualmente i codici di errore livello per livello, il che è ovviamente molto più facile da fare con le eccezioni. Il punto è che questo è per situazioni inaspettate e non gestibili .
Tuttavia, riguardo alla situazione 1 (in cui accade qualcosa di inaspettato e non gestibile, semplicemente non vuoi registrarlo), le eccezioni possono essere utili perché potresti aggiungere informazioni contestuali . Ad esempio, se ottengo un'eccezione SqlException nei miei helper dati di livello inferiore, voglio rilevare quell'errore nel livello basso (dove conosco il comando SQL che ha causato l'errore) in modo da poter acquisire tali informazioni e rilanciare con informazioni aggiuntive . Si prega di notare la parola magica qui: rigirare e non ingoiare .
La prima regola per gestire le eccezioni: non ingoiare eccezioni . Inoltre, nota che il mio catch interno non ha bisogno di registrare nulla perché il catch esterno avrà l'intera traccia dello stack e potrebbe registrarlo.
In alcune situazioni hai una sequenza di comandi e, se qualcuno di essi fallisce , dovresti pulire / smaltire le risorse (*), indipendentemente dal fatto che si tratti di una situazione irrecuperabile (che dovrebbe essere lanciata) o di una situazione recuperabile (nel qual caso puoi gestire localmente o nel codice del chiamante ma non servono eccezioni). Ovviamente è molto più facile mettere tutti quei comandi in un unico tentativo, invece di testare i codici di errore dopo ogni metodo, e pulire / eliminare nel blocco finalmente. Si prega di notare che se si desidera che l'errore si sporga (che è probabilmente quello che si desidera), non è nemmeno necessario catturarlo: è sufficiente utilizzare finalmente per ripulire / smaltire - si dovrebbe usare solo catch / retrow se lo si desidera per aggiungere informazioni contestuali (vedere punto 2).
Un esempio potrebbe essere una sequenza di istruzioni SQL all'interno di un blocco di transazione. Di nuovo, anche questa è una situazione "non gestibile", anche se decidi di prenderla presto (trattala localmente invece di ribollire fino in cima) è ancora una situazione fatale in cui il miglior risultato è abortire tutto o almeno abortire un grande parte del processo.
(*) Questo è come on error goto
quello che usavamo nel vecchio Visual Basic
Nei costruttori puoi solo generare eccezioni.
Detto questo, in tutte le altre situazioni in cui stai restituendo alcune informazioni su cui il chiamante PUO / DOVREBBE intraprendere un'azione , l'uso dei codici di ritorno è probabilmente un'alternativa migliore. Questo include tutti gli "errori" previsti , perché probabilmente dovrebbero essere gestiti dal chiamante immediato e difficilmente avranno bisogno di essere ribolliti di troppi livelli nello stack.
Ovviamente è sempre possibile trattare gli errori attesi come eccezioni e quindi intercettare immediatamente un livello superiore, ed è anche possibile racchiudere ogni riga di codice in un try catch e intraprendere azioni per ogni possibile errore. IMO, questo è un cattivo design, non solo perché è molto più prolisso, ma soprattutto perché le possibili eccezioni che potrebbero essere lanciate non sono ovvie senza leggere il codice sorgente - e le eccezioni potrebbero essere lanciate da qualsiasi metodo profondo, creando gotos invisibili . Rompono la struttura del codice creando più punti di uscita invisibili che rendono il codice difficile da leggere e ispezionare. In altre parole, non dovresti mai usare eccezioni come controllo del flusso , perché sarebbe difficile da capire e mantenere per gli altri. Può diventare persino difficile capire tutti i possibili flussi di codice per i test.
Anche in questo caso: per una corretta pulizia / eliminazione è possibile utilizzare Try-Finally senza prendere nulla .
La critica più popolare sui codici di ritorno è che "qualcuno potrebbe ignorare i codici di errore, ma nello stesso senso qualcuno può anche ingoiare le eccezioni. Una cattiva gestione delle eccezioni è facile in entrambi i metodi. Ma scrivere un buon programma basato sul codice di errore è ancora molto più semplice che scrivere un programma basato su eccezioni e se uno per qualsiasi motivo decide di ignorare tutti gli errori (il vecchio on error resume next
), puoi farlo facilmente con i codici di ritorno e non puoi farlo senza un sacco di bozze di try-catch.
La seconda critica più popolare sui codici di ritorno è che "è difficile creare bolle", ma questo perché le persone non capiscono che le eccezioni sono per situazioni non recuperabili, mentre i codici di errore non lo sono.
La scelta tra eccezioni e codici di errore è un'area grigia. È anche possibile che sia necessario ottenere un codice di errore da un metodo aziendale riutilizzabile, quindi decidere di racchiuderlo in un'eccezione (possibilmente aggiungendo informazioni) e lasciarlo gonfiare. Ma è un errore di progettazione presumere che TUTTI gli errori debbano essere lanciati come eccezioni.
Riassumendo:
Mi piace usare le eccezioni quando ho una situazione inaspettata, in cui non c'è molto da fare e di solito vogliamo interrompere un grosso blocco di codice o anche l'intera operazione o programma. Questo è come il vecchio "in caso di errore goto".
Mi piace utilizzare i codici di ritorno quando ho previsto situazioni in cui il codice del chiamante può / dovrebbe intraprendere un'azione. Ciò include la maggior parte dei metodi aziendali, API, convalide e così via.
Questa differenza tra eccezioni e codici di errore è uno dei principi di progettazione del linguaggio GO, che utilizza "panico" per situazioni impreviste fatali, mentre le situazioni previste regolari vengono restituite come errori.
Tuttavia, riguardo a GO, consente anche più valori di ritorno , il che è qualcosa che aiuta molto nell'utilizzo dei codici di ritorno, poiché è possibile restituire contemporaneamente un errore e qualcos'altro. Su C # / Java possiamo ottenerlo senza parametri, tuple o (i miei preferiti) generici, che combinati con le enumerazioni possono fornire chiari codici di errore al chiamante:
public MethodResult<CreateOrderResultCodeEnum, Order> CreateOrder(CreateOrderOptions options)
{
....
return MethodResult<CreateOrderResultCodeEnum>.CreateError(CreateOrderResultCodeEnum.NO_DELIVERY_AVAILABLE, "There is no delivery service in your area");
...
return MethodResult<CreateOrderResultCodeEnum>.CreateSuccess(CreateOrderResultCodeEnum.SUCCESS, order);
}
var result = CreateOrder(options);
if (result.ResultCode == CreateOrderResultCodeEnum.OUT_OF_STOCK)
// do something
else if (result.ResultCode == CreateOrderResultCodeEnum.SUCCESS)
order = result.Entity; // etc...
Se aggiungo un nuovo possibile ritorno nel mio metodo, posso anche controllare tutti i chiamanti se coprono quel nuovo valore in un'istruzione switch, ad esempio. Non puoi davvero farlo con le eccezioni. Quando utilizzi i codici di ritorno, di solito conoscerai in anticipo tutti i possibili errori e li verificherai. Con le eccezioni di solito non sai cosa potrebbe accadere. Il wrapping di enumerazioni all'interno di eccezioni (invece di Generics) è un'alternativa (purché sia chiaro il tipo di eccezioni che ogni metodo genererà), ma IMO è ancora un cattivo design.