Propagazione delle eccezioni: quando devo rilevare le eccezioni?


44

MethodA chiama un MethodB che a sua volta chiama MethodC.

Non esiste una gestione delle eccezioni in MethodB o MethodC. Ma c'è una gestione delle eccezioni in MethodA.

In MethodC si verifica un'eccezione.

Ora, quell'eccezione sta ribollendo fino a MethodA, che la gestisce in modo appropriato.

Cosa c'è di sbagliato in questo?

Nella mia mente, a un certo punto un chiamante eseguirà MethodB o MethodC, e quando si verificano eccezioni in quei metodi, ciò che si otterrà dalla gestione delle eccezioni all'interno di quei metodi, che essenzialmente è solo un blocco try / catch / finally invece di lasciare semplicemente si gonfiano fino alla chiamata?

L'affermazione o il consenso sulla gestione delle eccezioni è quando l'esecuzione non può continuare proprio per questo - un'eccezione. Capisco quello. Ma perché non catturare l'eccezione più in alto della catena invece di provare / catturare i blocchi fino in fondo.

Lo capisco quando devi liberare risorse. Questa è una questione completamente diversa.


46
Perché pensi che il consenso sia quello di avere una catena di passaggio attraverso le catture?
Caleth,

Con un buon IDE e uno stile di codifica adeguato, puoi sapere che è possibile generare qualche eccezione quando viene chiamato un metodo. Gestirlo o consentirne la propagazione è la decisione del chiamante. Non vedo alcun problema con questo.
Hieu Le

14
Se un metodo non è in grado di gestire l'eccezione e la sta semplicemente ridisegnando, direi che è un odore di codice. Se un metodo non è in grado di gestire l'eccezione e non deve fare altro quando viene generata un'eccezione, non è necessario alcun try-catchblocco.
Greg Burghardt

7
"Cosa c'è di sbagliato in questo?" : niente
Ewan

5
La cattura pass-through (che non racchiude eccezioni in tipi diversi o simili) sconfigge l'intero scopo delle eccezioni. Il lancio di eccezioni è un meccanismo complesso ed è stato costruito intenzionalmente. Se le catture pass-through fossero il caso d'uso previsto, tutto ciò di cui avresti bisogno è implementare un Result<T>tipo (un tipo che memorizza un risultato di un calcolo o un errore) e restituirlo dalle tue funzioni di lancio altrimenti. La propagazione di un errore nello stack comporterebbe la lettura di tutti i valori restituiti, il controllo di eventuali errori e la restituzione di un errore in tal caso.
Alexander

Risposte:


139

Come principio generale, non prendere eccezioni a meno che tu non sappia cosa farne. Se MethodC genera un'eccezione, ma MethodB non ha un modo utile per gestirla, dovrebbe consentire all'eccezione di propagarsi fino a MethodA.

Le uniche ragioni per cui un metodo dovrebbe avere un meccanismo di cattura e riprogrammazione sono:

  • Si desidera convertire un'eccezione in un'altra diversa che sia più significativa per il chiamante sopra.
  • Vuoi aggiungere ulteriori informazioni all'eccezione.
  • È necessaria una clausola di cattura per ripulire le risorse che sarebbero trapelate senza una.

Altrimenti, la cattura di eccezioni al livello sbagliato tende a provocare un codice che fallisce silenziosamente senza fornire alcun feedback utile al codice chiamante (e in definitiva all'utente del software). L'alternativa di catturare un'eccezione e poi riproporla immediatamente è inutile.


28
@GregBurghardt se la tua lingua ha qualcosa di simile try ... finally ..., quindi usalo, non catturare e ricominciare
Caleth

19
"catturare un'eccezione e quindi riproporla immediatamente è inutile" A seconda della lingua e di come si procede, può essere attivamente dannoso per la base di codice. Spesso le persone che tentano questo rimuovono molte informazioni sull'eccezione come lo stack stack originale. Mi sono occupato di codice in cui il chiamante riceve un'eccezione che è completamente fuorviante su ciò che è accaduto e dove.
JimmyJames

7
"Non cogliere eccezioni se non sai cosa farne". A prima vista sembra ragionevole, ma in seguito causa problemi. Quello che stai facendo qui è la perdita dei dettagli di implementazione per i tuoi chiamanti. Immagina di utilizzare un particolare ORM nella tua implementazione per caricare i dati. Se non si rilevano le eccezioni specifiche di ORM ma le si lasciano solo sfogliare, non è possibile sostituire il livello dati senza interrompere la compatibilità con gli utenti esistenti. Questo è uno dei casi più ovvi, ma può diventare abbastanza insidioso ed è difficile da capire.
Voo

11
@Voo Nel tuo esempio si fa a sapere cosa fare con esso. Inseriscilo in un'eccezione documentata specifica per il tuo codice, ad esempio LoadDataExceptione includi i dettagli dell'eccezione originale in base alle caratteristiche della tua lingua, in modo che i futuri manutentori siano in grado di vedere la causa principale senza dover collegare un debugger e capire come riprodurre il problema.
Colin Young,

14
@Voo Sembra che tu abbia perso il motivo "Vuoi convertire un'eccezione in un'altra diversa che è più significativa per il chiamante sopra" per gli scenari di cattura / ripescaggio.
jpmc26

21

Cosa c'è di sbagliato in questo?

Assolutamente niente.

Ora, quell'eccezione sta ribollendo fino a MethodA, che la gestisce in modo appropriato.

"gestisce in modo appropriato" è la parte importante. Questo è il punto cruciale della gestione delle eccezioni strutturata.

Se il tuo codice può fare qualcosa di "utile" con un'eccezione, provaci. Altrimenti, figuriamoci bene.

. . . perché non catturare l'eccezione più in alto della catena invece di provare / catturare i blocchi fino in fondo.

Questo è esattamente quello che dovresti fare. Se stai leggendo il codice che ha il gestore / i rethrower "fino in fondo", allora [probabilmente] stai leggendo un codice piuttosto scadente.

Purtroppo, alcuni sviluppatori vedono solo i blocchi di cattura come codice "boiler-plate" che inseriscono (nessun gioco di parole inteso) in ogni metodo che scrivono, spesso perché in realtà non "ottengono" la gestione delle eccezioni e pensano di dover aggiungere qualcosa così che le eccezioni non "sfuggono" e uccidono il loro programma.

Parte della difficoltà qui è che, il più delle volte, questo problema non verrà nemmeno notato, perché le Eccezioni non vengono lanciate tutto il tempo ma, quando lo sono , il programma sta per perdere un sacco di tempo e sforzo gradualmente decomprimendo lo stack di chiamate per arrivare da qualche parte che effettivamente fa qualcosa di utile con l'eccezione.


7
Ciò che è ancora peggio è quando l'applicazione rileva l'eccezione, quindi la registra (dove si spera non rimarrà lì per sempre) e tenta di continuare come al solito, anche quando davvero non può.
Solomon Ucko,

1
@ SolomonUcko: Beh, dipende. Se, ad esempio, stai scrivendo un semplice server RPC e un'eccezione non gestita arriva fino al ciclo degli eventi principale, l'unica opzione ragionevole è registrarlo, inviare un errore RPC al peer remoto e riprendere l'elaborazione degli eventi. L'altra alternativa è quella di uccidere l'intero programma, che farà impazzire i tuoi SRE quando il server muore in produzione.
Kevin,

@Kevin In tal caso, dovrebbe esserci un singolo catchal livello più alto possibile che registra l'errore e restituisce una risposta di errore. Non catchblocchi sparsi ovunque. Se non hai voglia di elencare tutte le possibili eccezioni verificate (in lingue come Java), basta racchiuderlo in RuntimeExceptioninvece di registrarlo lì, tentare di continuare e incorrere in più errori o persino vulnerabilità.
Solomon Ucko,

8

Devi fare la differenza tra Librerie e Applicazioni.

Le biblioteche possono generare liberamente eccezioni non rilevate

Quando progetti una biblioteca, a un certo punto devi pensare a cosa può andare storto. I parametri potrebbero essere nell'intervallo errato o null, le risorse esterne potrebbero non essere disponibili, ecc.

La tua biblioteca il più delle volte non avrà modo di gestirli in modo sensato . L'unica soluzione ragionevole è lanciare un'eccezione appropriata e lasciare che lo sviluppatore dell'applicazione lo gestisca.

Le applicazioni dovrebbero sempre rilevare eccezioni

Quando viene rilevata un'eccezione, mi piace classificarli come errori o errori irreversibili . Un errore regolare significa che una singola operazione all'interno della mia applicazione non è riuscita. Ad esempio, non è stato possibile salvare un documento aperto perché la destinazione non era scrivibile. L'unica cosa sensata da fare per l'applicazione è informare l'utente che l'operazione non può essere completata con successo, fornire informazioni leggibili sull'uomo in merito al problema e quindi lasciare che l'utente decida cosa fare dopo.

Un errore irreversibile è un errore da cui la logica principale dell'applicazione non è in grado di ripristinare. Ad esempio, se il driver del dispositivo grafico si arresta in modo anomalo in un videogioco, l'applicazione non può in alcun modo informare "con grazia" l'utente. In questo caso, dovrebbe essere scritto un file di registro e, se possibile, l'utente dovrebbe essere informato in un modo o nell'altro.

Anche in un caso così grave, l'applicazione dovrebbe gestire questa eccezione in modo significativo. Ciò potrebbe includere la scrittura di un file di registro, l'invio di un rapporto di arresto anomalo, ecc. Non vi è alcun motivo per cui l'applicazione non risponda in qualche modo all'eccezione.


Infatti, se hai una libreria per qualcosa come operazioni di scrittura su disco o, diciamo, altre manipolazioni hardware, puoi finire in tutti i tipi di eventi imprevisti. Cosa succede se un disco rigido viene strappato durante la scrittura? Quale unità CD si cortocircuita durante la lettura? Quel fuori del vostro controllo e, mentre si potrebbe fare qualcosa (per esempio, far finta che era successo), è spesso una buona pratica di un'eccezione per l'utente biblioteca e far loro decidere. Forse un HDDPluggedOutDuringWritingExceptionpuò essere affrontato ed è non fatale per l'applicazione. Il programma può decidere cosa farne.
VLAZ,

1
@VLAZ Ciò che è fatale o non fatale è qualcosa che l'applicazione deve decidere. La biblioteca dovrebbe dire cosa è successo. L'applicazione deve decidere come reagire ad essa.
MechMK1

0

Ciò che è sbagliato nel modello che descrivi è che il metodo A non avrà modo di distinguere tra tre scenari:

  1. Il metodo B ha fallito in modo anticipato.

  2. Il metodo C fallì in un modo non previsto dal metodo B, ma mentre il metodo B stava eseguendo un'operazione che poteva essere tranquillamente abbandonata.

  3. Il metodo C fallì in un modo non previsto dal metodo B, ma mentre il metodo B stava eseguendo un'operazione che poneva le cose in uno stato incoerente presumibilmente temporaneo che B non riusciva a ripulire a causa del fallimento di C.

L'unico modo in cui il metodo A sarà in grado di distinguere tali scenari sarà se l'eccezione generata da B include informazioni sufficienti a tale scopo o se lo stack che si svolge per il metodo B fa sì che l'oggetto venga lasciato in uno stato esplicitamente invalidato . Sfortunatamente, la maggior parte dei framework delle eccezioni rende entrambi i modelli imbarazzanti, costringendo i programmatori a prendere decisioni di progettazione "male minore".


2
Gli scenari 2 e 3 sono bug nel metodo B. Il metodo A non dovrebbe cercare di risolverli.
Sjoerd,

@Sjoerd: In che modo il metodo B dovrebbe anticipare tutti i modi in cui il metodo C potrebbe fallire?
supercat

Mediante schemi ben noti come l'esecuzione di tutte le operazioni che potrebbero essere lanciate in variabili temporanee, quindi con operazioni che non sono in grado di generare, ad esempio lo scambio, scambiare il vecchio con il nuovo stato. Un altro modello è la definizione di operazioni che possono essere ripetute in modo sicuro, quindi è possibile riprovare l'operazione senza paura di incasinare. Ci sono libri completi sulla scrittura di "codice sicuro di eccezione", quindi non posso dirti tutto qui.
Sjoerd,

Questo è un buon punto per non usare affatto le eccezioni (che sarebbe una scelta eccellente imho). Ma suppongo che non risponda davvero alla domanda, poiché l'OP sembra intenzionato a utilizzare le eccezioni in primo luogo e chiede solo dove dovrebbe essere la cattura.
cmaster

@Sjoerd Il metodo B diventa molto più facile da ragionare se le eccezioni sono proibite dalla lingua. Perché in quel caso, in realtà vedi tutti i percorsi del flusso di controllo attraverso B e non devi indovinare quali operatori potrebbero essere sovraccaricati in modo da lanciare (C ++) per evitare lo scenario 3. Stiamo pagando molto in termini di codice chiarezza e sicurezza per essere pigri riguardo alla restituzione degli errori "solo" generando eccezioni. Perché, alla fine, la gestione degli errori è una parte vitale del codice.
cmaster
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.