Quale e perché preferisci le eccezioni o i codici di ritorno?


92

La mia domanda è cosa preferisce la maggior parte degli sviluppatori per la gestione degli errori, le eccezioni o i codici di ritorno degli errori. Si prega di essere specifici per lingua (o famiglia linguistica) e perché si preferisce uno rispetto all'altro.

Lo chiedo per curiosità. Personalmente preferisco i codici di errore di ritorno poiché sono meno esplosivi e non costringono il codice utente a pagare la penalità per le prestazioni dell'eccezione se non lo desiderano.

aggiornamento: grazie per tutte le risposte! Devo dire che anche se non mi piace l'imprevedibilità del flusso di codice con eccezioni. La risposta sul codice di ritorno (e le maniglie del fratello maggiore) aggiunge molto rumore al codice.


1
Un problema di pollo o uovo dal mondo del software ... eternamente discutibile. :)
Gishu

Mi dispiace, ma si spera che avere una varietà di opinioni aiuterà le persone (me compreso) a scegliere in modo appropriato.
Robert Gould

Risposte:


107

Per alcuni linguaggi (ad esempio C ++) la perdita di risorse non dovrebbe essere un motivo

Il C ++ è basato su RAII.

Se hai del codice che potrebbe fallire, restituire o lanciare (cioè, il codice più normale), allora dovresti avere il tuo puntatore avvolto in un puntatore intelligente (supponendo che tu abbia un'ottima ragione per non avere il tuo oggetto creato sullo stack).

I codici di ritorno sono più dettagliati

Sono prolissi e tendono a svilupparsi in qualcosa di simile:

if(doSomething())
{
   if(doSomethingElse())
   {
      if(doSomethingElseAgain())
      {
          // etc.
      }
      else
      {
         // react to failure of doSomethingElseAgain
      }
   }
   else
   {
      // react to failure of doSomethingElse
   }
}
else
{
   // react to failure of doSomething
}

Alla fine, il tuo codice è una raccolta di istruzioni identificate (ho visto questo tipo di codice nel codice di produzione).

Questo codice potrebbe essere tradotto in:

try
{
   doSomething() ;
   doSomethingElse() ;
   doSomethingElseAgain() ;
}
catch(const SomethingException & e)
{
   // react to failure of doSomething
}
catch(const SomethingElseException & e)
{
   // react to failure of doSomethingElse
}
catch(const SomethingElseAgainException & e)
{
   // react to failure of doSomethingElseAgain
}

Che separa in modo pulito il codice e l'elaborazione degli errori, il che può essere una buona cosa.

I codici di ritorno sono più fragili

Se non qualche oscuro avvertimento da un compilatore (vedere il commento di "phjr"), possono essere facilmente ignorati.

Con gli esempi precedenti, supponi che qualcuno dimentichi di gestire il suo possibile errore (questo accade ...). L'errore viene ignorato quando "restituito" e probabilmente esploderà in seguito (cioè un puntatore NULL). Lo stesso problema non si verificherà eccezionalmente.

L'errore non verrà ignorato. A volte, però, vuoi che non esploda ... Quindi devi scegliere con attenzione.

A volte i codici di ritorno devono essere tradotti

Supponiamo di avere le seguenti funzioni:

  • doSomething, che può restituire un int chiamato NOT_FOUND_ERROR
  • doSomethingElse, che può restituire un bool "false" (per non riuscito)
  • doSomethingElseAgain, che può restituire un oggetto Error (con entrambe le variabili __LINE__, __FILE__ e metà dello stack.
  • doTryToDoSomethingWithAllThisMess che, beh ... Usa le funzioni precedenti e restituisce un codice di errore di tipo ...

Qual è il tipo di ritorno di doTryToDoSomethingWithAllThisMess se una delle sue funzioni chiamate fallisce?

I codici di ritorno non sono una soluzione universale

Gli operatori non possono restituire un codice di errore. Anche i costruttori C ++ non possono.

Codici di ritorno significa che non puoi concatenare espressioni

Il corollario del punto precedente. E se volessi scrivere:

CMyType o = add(a, multiply(b, c)) ;

Non posso, perché il valore di ritorno è già utilizzato (e talvolta non può essere modificato). Quindi il valore restituito diventa il primo parametro, inviato come riferimento ... Oppure no.

Le eccezioni vengono digitate

Puoi inviare classi differenti per ogni tipo di eccezione. Le eccezioni sulle risorse (cioè la memoria insufficiente) dovrebbero essere leggere, ma qualsiasi altra cosa potrebbe essere pesante quanto necessario (mi piace l'eccezione Java che mi dà l'intero stack).

Ogni cattura può quindi essere specializzata.

Non usare mai la cattura (...) senza rilanciare

Di solito, non dovresti nascondere un errore. Se non rilanci, almeno, registra l'errore in un file, apri una finestra di messaggio, qualunque cosa ...

Le eccezioni sono ... NUKE

Il problema con l'eccezione è che un uso eccessivo produrrà codice pieno di tentativi / catture. Ma il problema è altrove: chi prova / cattura il suo codice usando il contenitore STL? Tuttavia, quei contenitori possono inviare un'eccezione.

Ovviamente, in C ++, non lasciare mai che un'eccezione esca da un distruttore.

Le eccezioni sono ... sincrone

Assicurati di catturarli prima che tirino fuori il tuo thread in ginocchio o si propagino all'interno del loop dei messaggi di Windows.

La soluzione potrebbe essere mescolarli?

Quindi immagino che la soluzione sia lanciare quando qualcosa non dovrebbe accadere. E quando può succedere qualcosa, utilizza un codice di ritorno o un parametro per consentire all'utente di reagire.

Quindi, l'unica domanda è "cosa è qualcosa che non dovrebbe accadere?"

Dipende dal contratto della tua funzione. Se la funzione accetta un puntatore, ma specifica che il puntatore deve essere diverso da NULL, allora va bene lanciare un'eccezione quando l'utente invia un puntatore NULL (la domanda è, in C ++, quando l'autore della funzione non usa invece riferimenti di suggerimenti, ma ...)

Un'altra soluzione sarebbe mostrare l'errore

A volte, il tuo problema è che non vuoi errori. Usare eccezioni o codici di ritorno di errore è interessante, ma ... vuoi saperlo.

Nel mio lavoro usiamo una sorta di "Assert". A seconda dei valori di un file di configurazione, indipendentemente dalle opzioni di compilazione di debug / rilascio:

  • registra l'errore
  • apri una casella di messaggi con un "Ehi, hai un problema"
  • apri una casella di messaggi con un "Ehi, hai un problema, vuoi eseguire il debug"

Sia nello sviluppo che nel test, questo consente all'utente di individuare il problema esattamente quando viene rilevato e non dopo (quando un codice si preoccupa del valore restituito o all'interno di un catch).

È facile da aggiungere al codice legacy. Per esempio:

void doSomething(CMyObject * p, int iRandomData)
{
   // etc.
}

conduce una sorta di codice simile a:

void doSomething(CMyObject * p, int iRandomData)
{
   if(iRandomData < 32)
   {
      MY_RAISE_ERROR("Hey, iRandomData " << iRandomData << " is lesser than 32. Aborting processing") ;
      return ;
   }

   if(p == NULL)
   {
      MY_RAISE_ERROR("Hey, p is NULL !\niRandomData is equal to " << iRandomData << ". Will throw.") ;
      throw std::some_exception() ;
   }

   if(! p.is Ok())
   {
      MY_RAISE_ERROR("Hey, p is NOT Ok!\np is equal to " << p->toString() << ". Will try to continue anyway") ;
   }

   // etc.
}

(Ho macro simili che sono attive solo su debug).

Nota che in produzione il file di configurazione non esiste, quindi il client non vede mai il risultato di questa macro ... Ma è facile attivarlo quando serve.

Conclusione

Quando codifichi usando i codici di ritorno, ti stai preparando al fallimento e speri che la tua fortezza dei test sia abbastanza sicura.

Quando si codifica utilizzando l'eccezione, si sa che il codice può fallire e di solito si inserisce il blocco del counterfire nella posizione strategica scelta nel codice. Ma di solito, il tuo codice riguarda più "cosa deve fare" che "ciò che temo accadrà".

Ma quando scrivi del codice, devi usare il miglior strumento a tua disposizione e, a volte, è "Non nascondere mai un errore e mostralo il prima possibile". La macro di cui ho parlato sopra segue questa filosofia.


3
Sì, MA riguardo al tuo primo esempio, potrebbe essere facilmente scritto come: if( !doSomething() ) { puts( "ERROR - doSomething failed" ) ; return ; // or react to failure of doSomething } if( !doSomethingElse() ) { // react to failure of doSomethingElse() }
bobobobo

Perché no ... Ma poi, trovo ancora di doSomething(); doSomethingElse(); ...meglio perché se devo aggiungere if / while / etc. istruzioni per scopi di esecuzione normale, non le voglio mischiare con if / while / etc. dichiarazioni aggiunte per scopi eccezionali ... E poiché la vera regola sull'uso delle eccezioni è lanciare , non prendere , le istruzioni try / catch di solito non sono invasive.
paercebal

1
Il tuo primo punto mostra qual è il problema con le eccezioni. Il tuo flusso di controllo diventa strano ed è separato dal problema reale. Sta sostituendo alcuni livelli di identificazione con una cascata di catture. Userei entrambi i codici di ritorno (o restituirei oggetti con informazioni pesanti) per possibili errori ed eccezioni per cose che non sono previste .
Peter

2
@Peter Weber: non è strano. È separato dal problema effettivo perché non fa parte del normale flusso di esecuzione. È un'esecuzione eccezionale . E poi, ancora una volta, il punto sull'eccezione è, in caso di errore eccezionale , lanciare spesso e catturare raramente , se mai. Quindi i blocchi di cattura appaiono raramente nel codice.
paercebal

Gli esempi in questo tipo di dibattito sono molto semplicistici. Di solito, "doSomething ()" o "doSomethingElse ()" eseguono effettivamente qualcosa, come cambiare lo stato di un oggetto. Il codice di eccezione non garantisce il ritorno dell'oggetto allo stato precedente e ancor meno quando il catch è molto lontano dal lancio ... Ad esempio, immagina che doSomething venga chiamato due volte e incrementa un contatore prima di lanciare. Come fai a sapere quando cogli l'eccezione che dovresti diminuire una o due volte? In generale, scrivere codice protetto dalle eccezioni per qualsiasi cosa che non sia un esempio di giocattolo è molto difficile (impossibile?).
xryl669

36

In realtà li uso entrambi.

Uso i codici di ritorno se si tratta di un possibile errore noto. Se è uno scenario che so può accadere e accadrà, allora c'è un codice che viene inviato indietro.

Le eccezioni vengono utilizzate esclusivamente per cose che NON mi aspetto.


+1 per un modo molto semplice di procedere. Almeno è abbastanza breve in modo da poterlo leggere molto rapidamente. :-)
Ashish Gupta

1
"Le eccezioni vengono utilizzate esclusivamente per cose che NON mi aspetto. " Se non le aspetti, perché fare o come puoi usarle?
anar khalilov

5
Solo perché non mi aspetto che accada, non significa che non riesco a vedere come potrebbe. Mi aspetto che il mio SQL Server sia acceso e risponda. Ma continuo a codificare le mie aspettative in modo da poter fallire con garbo se si verifica un tempo di inattività imprevisto.
Stephen Wrighton

Un SQL Server che non risponde non sarebbe facilmente classificato come "noto, possibile errore"?
tkburbidge

21

Secondo il capitolo 7 intitolato "Eccezioni" in Linee guida per la progettazione di framework: convenzioni, idiomi e modelli per le librerie .NET riutilizzabili , vengono fornite numerose motivazioni sul motivo per cui è necessario utilizzare le eccezioni sui valori di ritorno per i framework OO come C #.

Forse questo è il motivo più convincente (pagina 179):

"Le eccezioni si integrano bene con i linguaggi orientati agli oggetti. I linguaggi orientati agli oggetti tendono a imporre vincoli alle firme dei membri che non sono imposti dalle funzioni nei linguaggi non OO. Ad esempio, nel caso di costruttori, sovraccarichi di operatori e proprietà, lo sviluppatore non ha scelta nel valore di ritorno. Per questo motivo, non è possibile standardizzare la segnalazione degli errori basata sul valore di ritorno per i framework orientati agli oggetti. Un metodo di segnalazione degli errori, come le eccezioni, che è fuori banda della firma del metodo è l'unica opzione. "


10

La mia preferenza (in C ++ e Python) è usare le eccezioni. Le funzionalità fornite dalla lingua lo rendono un processo ben definito per sollevare, catturare e (se necessario) rilanciare eccezioni, rendendo il modello facile da vedere e da usare. Concettualmente, è più pulito dei codici di ritorno, in quanto specifiche eccezioni possono essere definite dai loro nomi e avere informazioni aggiuntive che le accompagnano. Con un codice di ritorno, sei limitato al solo valore di errore (a meno che tu non voglia definire un oggetto ReturnStatus o qualcosa del genere).

A meno che il codice che stai scrivendo non sia critico in termini di tempo, il sovraccarico associato allo svolgimento dello stack non è abbastanza significativo da preoccuparti.


2
Ricorda che l'uso delle eccezioni rende più difficile l'analisi del programma.
Paweł Hajdan

7

Le eccezioni dovrebbero essere restituite solo se accade qualcosa che non ti aspettavi.

L'altro punto delle eccezioni, storicamente, è che i codici di ritorno sono intrinsecamente proprietari, a volte uno 0 potrebbe essere restituito da una funzione C per indicare il successo, a volte -1, o uno dei due per un errore con 1 per un successo. Anche quando sono enumerate, le enumerazioni possono essere ambigue.

Le eccezioni possono anche fornire molte più informazioni e in particolare spiegare bene "Qualcosa è andato storto, ecco cosa, una traccia dello stack e alcune informazioni di supporto per il contesto"

Detto questo, un codice di ritorno ben enumerato può essere utile per un insieme noto di risultati, un semplice "ecco i risultati della funzione, e ha funzionato in questo modo"


6

In Java, utilizzo (nel seguente ordine):

  1. Design-by-contract (assicurando che siano soddisfatte le condizioni preliminari prima di provare qualsiasi cosa che potrebbe fallire). Questo cattura la maggior parte delle cose e restituisco un codice di errore per questo.

  2. Restituzione di codici di errore durante l'elaborazione del lavoro (ed esecuzione del rollback se necessario).

  3. Eccezioni, ma queste vengono utilizzate solo per cose inaspettate.


1
Non sarebbe leggermente più corretto usare le asserzioni per i contratti? Se il contratto è rotto, non c'è niente che ti salvi.
Paweł Hajdan

@ PawełHajdan, le affermazioni sono disabilitate per impostazione predefinita, credo. Questo ha lo stesso problema delle assertcose di C in quanto non rileva problemi nel codice di produzione a meno che tu non esegua con asserzioni tutto il tempo. Tendo a vedere le asserzioni come un modo per individuare i problemi durante lo sviluppo, ma solo per cose che asseriranno o non asseriranno in modo coerente (come cose con costanti, non cose con variabili o qualsiasi altra cosa che può cambiare in fase di esecuzione).
paxdiablo

E dodici anni per rispondere alla tua domanda. Dovrei gestire un help desk :-)
paxdiablo

6

I codici di ritorno falliscono il test " Pit of Success " per me quasi ogni volta.

  • È fin troppo facile dimenticare di controllare un codice di ritorno e quindi avere un errore di falsificazione in seguito.
  • I codici di ritorno non hanno nessuna delle ottime informazioni di debug su di loro come stack di chiamate, eccezioni interne.
  • I codici di ritorno non si propagano il che, insieme al punto precedente, tende a generare una registrazione diagnostica eccessiva e intrecciata invece di eseguire la registrazione in un luogo centralizzato (gestori di eccezioni a livello di applicazione e thread).
  • I codici di ritorno tendono a generare codice disordinato sotto forma di blocchi "if" annidati
  • Il tempo dedicato dallo sviluppatore al debug di un problema sconosciuto che altrimenti sarebbe stato un'ovvia eccezione (pozzo del successo) È costoso.
  • Se il team dietro C # non intendesse che le eccezioni governino il flusso di controllo, le esecuzioni non verrebbero digitate, non ci sarebbe alcun filtro "quando" sulle istruzioni catch e non ci sarebbe bisogno dell'istruzione "throw" senza parametri .

Per quanto riguarda le prestazioni:

  • Le eccezioni possono essere computazionalmente costose rispetto a non lanciare affatto, ma sono chiamate ECCEZIONI per una ragione. I confronti di velocità riescono sempre a ipotizzare un tasso di eccezione del 100%, cosa che non dovrebbe mai essere il caso. Anche se un'eccezione è 100 volte più lenta, quanto conta davvero se si verifica solo l'1% delle volte?
  • A meno che non si parli di aritmetica in virgola mobile per applicazioni grafiche o qualcosa di simile, i cicli della CPU sono economici rispetto al tempo dello sviluppatore.
  • Il costo da una prospettiva temporale porta lo stesso argomento. Relativamente alle query del database, alle chiamate al servizio Web o al caricamento di file, il tempo normale dell'applicazione ridurrà il tempo di eccezione. Le eccezioni erano quasi inferiori al MICROsecondo nel 2006
    • Sfido chiunque lavori in .net, a impostare il tuo debugger in modo che rompa tutte le eccezioni e disabiliti solo il mio codice e vedi quante eccezioni stanno già accadendo di cui non sei nemmeno a conoscenza.

4

Un ottimo consiglio che ho ricevuto da The Pragmatic Programmer è stato qualcosa sulla falsariga di "il tuo programma dovrebbe essere in grado di eseguire tutte le sue funzionalità principali senza usare eccezioni".


4
Lo stai interpretando male. Quello che intendevano era "se il tuo programma genera eccezioni nei suoi flussi normali, è sbagliato". In altre parole "usa eccezioni solo per cose eccezionali".
Paweł Hajdan

4

Ho scritto un post sul blog a riguardo qualche tempo fa.

Il sovraccarico delle prestazioni di lanciare un'eccezione non dovrebbe avere alcun ruolo nella tua decisione. Se lo fai bene, dopotutto, un'eccezione è eccezionale .


Il post del blog collegato riguarda più l'aspetto prima di saltare (controlli) rispetto a chiedere più facilmente perdono che autorizzazione (eccezioni o codici di ritorno). Ho risposto lì con i miei pensieri su questo problema (suggerimento: TOCTTOU). Ma questa domanda riguarda un problema diverso, vale a dire in quali condizioni utilizzare il meccanismo di eccezione di un linguaggio piuttosto che restituire un valore con proprietà speciali.
Damian Yerrick

Sono completamente d'accordo. Sembra che abbia imparato una o due cose negli ultimi nove anni;)
Thomas

4

Non mi piacciono i codici di ritorno perché fanno sì che il seguente modello si diffonda in tutto il codice

CRetType obReturn = CODE_SUCCESS;
obReturn = CallMyFunctionWhichReturnsCodes();
if (obReturn == CODE_BLOW_UP)
{
  // bail out
  goto FunctionExit;
}

Presto una chiamata di metodo composta da 4 chiamate di funzione si gonfia con 12 righe di gestione degli errori .. Alcune delle quali non avverranno mai. Se e cambia i casi abbondano.

Le eccezioni sono più pulite se usate bene ... per segnalare eventi eccezionali .. dopodiché il percorso di esecuzione non può continuare. Sono spesso più descrittivi e informativi dei codici di errore.

Se dopo una chiamata al metodo sono presenti più stati che devono essere gestiti in modo diverso (e non sono casi eccezionali), utilizzare codici di errore o parametri out. Anche se personalmente ho scoperto che questo è raro ..

Ho cercato un po 'sulla controargomentazione della "penalizzazione delle prestazioni" .. più nel mondo C ++ / COM ma nei linguaggi più recenti, penso che la differenza non sia così tanto. In ogni caso, quando qualcosa esplode, i problemi di prestazioni vengono relegati in secondo piano :)


3

Con qualsiasi compilatore decente o le eccezioni dell'ambiente di runtime non si incorre in una penalità significativa. È più o meno come un'istruzione GOTO che salta al gestore delle eccezioni. Inoltre, avere eccezioni rilevate da un ambiente di runtime (come JVM) aiuta a isolare e correggere un bug molto più facilmente. Prenderò una NullPointerException in Java su un segfault in C ogni giorno.


2
Le eccezioni sono estremamente costose. Devono camminare sullo stack per trovare potenziali gestori di eccezioni. Questa passeggiata sullo stack non è economica. Se viene creata un'analisi dello stack, è ancora più costoso, poiché è necessario analizzare l'intero stack .
Derek Park

Sono sorpreso che i compilatori non siano in grado di determinare dove verrà rilevata l'eccezione almeno una volta. Inoltre, il fatto che un'eccezione alteri il flusso di codice rende più facile identificare esattamente dove si verifica un errore, a mio parere, compensa una penalizzazione delle prestazioni.
Kyle Cronin

Gli stack di chiamate possono diventare estremamente complessi in fase di esecuzione e i compilatori generalmente non eseguono questo tipo di analisi. Anche se lo facessero, dovresti comunque camminare sulla pila per ottenere una traccia. Dovresti anche srotolare lo stack per gestire finallyblocchi e distruttori per oggetti allocati nello stack .
Derek Park,

Tuttavia, sono d'accordo sul fatto che i vantaggi del debug delle eccezioni spesso compensano i costi delle prestazioni.
Derek Park

1
Derak Park, le eccezioni sono costose quando accadono. Questo è il motivo per cui non dovrebbero essere abusati. Ma quando non accadono, non costano praticamente nulla.
paercebal

3

Ho una semplice serie di regole:

1) Usa i codici di ritorno per le cose a cui ti aspetti che il tuo chiamante immediato reagisca.

2) Utilizzare eccezioni per errori di portata più ampia e ci si può ragionevolmente aspettare che vengano gestiti da qualcosa di molti livelli sopra il chiamante in modo che la consapevolezza dell'errore non debba filtrare attraverso molti livelli, rendendo il codice più complesso.

In Java ho usato solo eccezioni non controllate, le eccezioni controllate finiscono per essere solo un'altra forma di codice di ritorno e nella mia esperienza la dualità di ciò che potrebbe essere "restituito" da una chiamata al metodo era generalmente più un ostacolo che un aiuto.


3

Uso le eccezioni in python sia in circostanze eccezionali che non eccezionali.

Spesso è utile poter utilizzare un'eccezione per indicare che "la richiesta non può essere eseguita", invece di restituire un valore di errore. Significa che tu / sempre / sai che il valore di ritorno è del tipo giusto, invece di arbitrariamente None o NotFoundSingleton o qualcosa del genere. Ecco un buon esempio di dove preferisco utilizzare un gestore di eccezioni invece di un condizionale sul valore restituito.

try:
    dataobj = datastore.fetch(obj_id)
except LookupError:
    # could not find object, create it.
    dataobj = datastore.create(....)

L'effetto collaterale è che quando viene eseguito un datastore.fetch (obj_id), non devi mai controllare se il suo valore di ritorno è None, ottieni immediatamente quell'errore gratuitamente. Questo è in contrasto con l'argomento, "il tuo programma dovrebbe essere in grado di eseguire tutte le sue funzionalità principali senza usare affatto eccezioni".

Ecco un altro esempio di dove le eccezioni sono "eccezionalmente" utili, per scrivere codice per gestire il filesystem che non è soggetto a condizioni di competizione.

# wrong way:
if os.path.exists(directory_to_remove):
    # race condition is here.
    os.path.rmdir(directory_to_remove)

# right way:
try: 
    os.path.rmdir(directory_to_remove)
except OSError:
    # directory didn't exist, good.
    pass

Una chiamata di sistema invece di due, nessuna condizione di gara. Questo è un pessimo esempio perché ovviamente questo fallirà con un OSError in più circostanze di quante la directory non esiste, ma è una soluzione "abbastanza buona" per molte situazioni strettamente controllate.


Il secondo esempio è fuorviante. Il modo presumibilmente sbagliato è sbagliato perché il codice os.path.rmdir è progettato per generare un'eccezione. L'implementazione corretta nel flusso del codice di ritorno sarebbe "if rmdir (...) == FAILED: pass"
MaR

3

Credo che i codici di ritorno aggiungano rumore al codice. Ad esempio, ho sempre odiato l'aspetto del codice COM / ATL a causa dei codici di ritorno. Ci doveva essere un controllo HRESULT per ogni riga di codice. Considero il codice di ritorno dell'errore una delle decisioni sbagliate prese dagli architetti di COM. Rende difficile il raggruppamento logico del codice, quindi la revisione del codice diventa difficile.

Non sono sicuro del confronto delle prestazioni quando è presente un controllo esplicito per il codice di ritorno in ogni riga.


1
Wad COM progettato per essere utilizzabile da linguaggi che non supportano le eccezioni.
Kevin

Questo è un buon punto. Ha senso trattare i codici di errore per i linguaggi di scripting. Almeno VB6 nasconde bene i dettagli del codice di errore incapsulandoli nell'oggetto Err, il che aiuta in qualche modo a un codice più pulito.
rpattabi

Non sono d'accordo: VB6 registra solo l'ultimo errore. In combinazione con il famigerato "in caso di errore riprendi dopo", ti perderai del tutto l'origine del tuo problema, nel momento in cui lo vedrai. Si noti che questa è la base della gestione degli errori in Win32 APII (vedere la funzione GetLastError)
paercebal

2

Preferisco usare le eccezioni per la gestione degli errori e restituire valori (o parametri) come il normale risultato di una funzione. Ciò fornisce uno schema di gestione degli errori semplice e coerente e, se eseguito correttamente, rende il codice dall'aspetto molto più pulito.


2

Una delle grandi differenze è che le eccezioni ti costringono a gestire un errore, mentre i codici di ritorno degli errori possono essere deselezionati.

I codici di errore restituiti, se utilizzati in modo pesante, possono anche causare codice molto brutto con molti test if simili a questo modulo:

if(function(call) != ERROR_CODE) {
    do_right_thing();
}
else {
    handle_error();
}

Personalmente preferisco usare eccezioni per errori che DOVREBBE o DEVE essere risolti dal codice chiamante, e utilizzare solo codici di errore per "errori previsti" in cui restituire qualcosa è effettivamente valido e possibile.


Almeno in C / C ++ e gcc puoi dare a una funzione un attributo che genererà un avviso quando il suo valore di ritorno viene ignorato.
Paweł Hajdan

phjr: Sebbene non sia d'accordo con il modello "restituisci codice di errore", il tuo commento dovrebbe forse diventare una risposta completa. Lo trovo abbastanza interessante. Per lo meno, mi ha dato un'informazione utile.
paercebal

2

Ci sono molte ragioni per preferire le eccezioni al codice di ritorno:

  • Di solito, per leggibilità, le persone cercano di ridurre al minimo il numero di dichiarazioni di ritorno in un metodo. In questo modo, le eccezioni impediscono di svolgere un lavoro extra mentre si è in uno stato incooretto e quindi impediscono di danneggiare potenzialmente più dati.
  • Le eccezioni sono generalmente più dettagliate e più facilmente estendibili rispetto al valore restituito. Supponi che un metodo restituisca un numero naturale e che utilizzi numeri negativi come codice di ritorno quando si verifica un errore, se l'ambito del tuo metodo cambia e ora restituisce numeri interi, dovrai modificare tutte le chiamate al metodo invece di modificare solo un po ' l'eccezione.
  • Le eccezioni consentono di separare più facilmente la gestione degli errori dal comportamento normale. Consentono di garantire che alcune operazioni funzionino in qualche modo come un'operazione atomica.

2

Le eccezioni non riguardano la gestione degli errori, IMO. Le eccezioni sono proprio questo; eventi eccezionali che non ti aspettavi. Usa con cautela dico.

I codici di errore possono essere OK, ma la restituzione di 404 o 200 da un metodo non è valida, IMO. Usa invece enumerazioni (.Net), che rende il codice più leggibile e più facile da usare per altri sviluppatori. Inoltre non è necessario mantenere una tabella su numeri e descrizioni.

Anche; il pattern try-catch-latest è un anti-pattern nel mio libro. Try-finalmente può essere buono, try-catch può anche essere buono, ma try-catch-finalmente non è mai buono. try-finalmente può spesso essere sostituito da un'istruzione "using" (modello IDispose), che è meglio IMO. E prova a catturare dove effettivamente catturi un'eccezione che sei in grado di gestire è buono, o se lo fai:

try{
    db.UpdateAll(somevalue);
}
catch (Exception ex) {
    logger.Exception(ex, "UpdateAll method failed");
    throw;
}

Quindi, finché lasci che l'eccezione continui a ribollire, va bene. Un altro esempio è questo:

try{
    dbHasBeenUpdated = db.UpdateAll(somevalue); // true/false
}
catch (ConnectionException ex) {
    logger.Exception(ex, "Connection failed");
    dbHasBeenUpdated = false;
}

Qui gestisco effettivamente l'eccezione; quello che faccio al di fuori del tentativo di cattura quando il metodo di aggiornamento fallisce è un'altra storia, ma penso che il mio punto sia stato chiarito. :)

Perché allora try-catch-finalmente è un anti-pattern? Ecco perché:

try{
    db.UpdateAll(somevalue);
}
catch (Exception ex) {
    logger.Exception(ex, "UpdateAll method failed");
    throw;
}
finally {
    db.Close();
}

Cosa succede se l'oggetto db è già stato chiuso? Viene lanciata una nuova eccezione e deve essere gestita! Questo è meglio:

try{
    using(IDatabase db = DatabaseFactory.CreateDatabase()) {
        db.UpdateAll(somevalue);
    }
}
catch (Exception ex) {
    logger.Exception(ex, "UpdateAll method failed");
    throw;
}

Oppure, se l'oggetto db non implementa IDisposable, fai questo:

try{
    try {
        IDatabase db = DatabaseFactory.CreateDatabase();
        db.UpdateAll(somevalue);
    }
    finally{
        db.Close();
    }
}
catch (DatabaseAlreadyClosedException dbClosedEx) {
    logger.Exception(dbClosedEx, "Database connection was closed already.");
}
catch (Exception ex) {
    logger.Exception(ex, "UpdateAll method failed");
    throw;
}

Comunque sono i miei 2 centesimi! :)


Sarà strano se un oggetto ha .Close () ma non ha .Dispose ()
abatishchev

Sto solo usando Close () come esempio. Sentiti libero di pensarlo come qualcos'altro. Come affermo; il pattern using dovrebbe essere usato (doh!) se disponibile. Questo ovviamente implica che la classe implementa IDisposable e come tale si potrebbe chiamare Dispose.
noocyte

1

Uso solo eccezioni, nessun codice di ritorno. Sto parlando di Java qui.

La regola generale che seguo è che se ho un metodo chiamato, doFoo()ne segue che se non "fa pippo", per così dire, allora è successo qualcosa di eccezionale e dovrebbe essere lanciata un'eccezione.


1

Una cosa che temo delle eccezioni è che il lancio di un'eccezione rovinerà il flusso del codice. Ad esempio se lo fai

void foo()
{
  MyPointer* p = NULL;
  try{
    p = new PointedStuff();
    //I'm a module user and  I'm doing stuff that might throw or not

  }
  catch(...)
  {
    //should I delete the pointer?
  }
}

O peggio ancora, se avessi cancellato qualcosa che non avrei dovuto, ma venissi lanciato per catturare prima di fare il resto della pulizia. Il lancio ha messo molto peso sul povero utente IMHO.


Questo è lo scopo dell'istruzione <code> finalmente </code>. Ma, ahimè, non è nello standard C ++ ...
Thomas

In C ++ dovresti attenersi alla regola pratica "Acquisire risorse in construcotor e rilasciarle in destrucotor. Per questo caso particolare auto_ptr funzionerà perfettamente.
Serge

Thomas, ti sbagli. Il C ++ non ha finalmente perché non ne ha bisogno. Invece ha RAII. La soluzione di Serge è una soluzione che utilizza RAII.
paercebal

Robert, usa la soluzione di Serge e scoprirai che il tuo problema sta scomparendo. Ora, se scrivi più tentativi / prese che lanci, allora (a giudicare dal tuo commento) forse hai un problema nel tuo codice. Ovviamente, usare catch (...) senza un rilancio di solito è un male, poiché nasconde l'errore per ignorarlo meglio.
paercebal

1

La mia regola generale nell'eccezione rispetto all'argomento del codice di ritorno:

  • Utilizzare i codici di errore quando è necessaria la localizzazione / internazionalizzazione: in .NET, è possibile utilizzare questi codici di errore per fare riferimento a un file di risorse che visualizzerà quindi l'errore nella lingua appropriata. Altrimenti, usa le eccezioni
  • Usa le eccezioni solo per gli errori veramente eccezionali. Se è qualcosa che accade abbastanza spesso, usa un codice di errore booleano o enum.

Non c'è motivo per cui non sia possibile utilizzare un'eccezione quando si esegue l10n / i18n. Le eccezioni possono contenere anche informazioni localizzate.
gizmo

1

Non trovo che i codici di ritorno siano meno brutti delle eccezioni. Con l'eccezione, hai il try{} catch() {} finally {}dove come i codici di ritorno che hai if(){}. Temevo eccezioni per le ragioni fornite nel post; non sai se il puntatore deve essere cancellato, cosa hai. Ma penso che tu abbia gli stessi problemi quando si tratta dei codici di ritorno. Non conosci lo stato dei parametri a meno che tu non conosca alcuni dettagli sulla funzione / metodo in questione.

Indipendentemente da ciò, devi gestire l'errore se possibile. Puoi lasciare che un'eccezione si propaghi al livello superiore con la stessa facilità con cui ignorare un codice di ritorno e lasciare che il programma segfault.

Mi piace l'idea di restituire un valore (enumerazione?) Per i risultati e un'eccezione per un caso eccezionale.


0

Per un linguaggio come Java, sceglierei Exception perché il compilatore restituisce un errore in fase di compilazione se le eccezioni non vengono gestite, costringendo la funzione chiamante a gestire / lanciare le eccezioni.

Per Python, sono più conflittuale. Non esiste un compilatore, quindi è possibile che il chiamante non gestisca l'eccezione generata dalla funzione che porta alle eccezioni di runtime. Se utilizzi codici di ritorno potresti avere un comportamento imprevisto se non gestito correttamente e se utilizzi eccezioni potresti ottenere eccezioni di runtime.


0

In genere preferisco i codici di ritorno perché consentono al chiamante di decidere se l'errore è eccezionale .

Questo approccio è tipico della lingua Elixir.

# I care whether this succeeds. If it doesn't return :ok, raise an exception.
:ok = File.write(path, content)

# I don't care whether this succeeds. Don't check the return value.
File.write(path, content)

# This had better not succeed - the path should be read-only to me.
# If I get anything other than this error, raise an exception.
{:error, :erofs} = File.write(path, content)

# I want this to succeed but I can handle its failure
case File.write(path, content) do
  :ok => handle_success()
  error => handle_error(error)
end

Le persone hanno affermato che i codici di ritorno possono farti avere molte ifistruzioni annidate , ma ciò può essere gestito con una sintassi migliore. In Elixir, l' withistruzione ci consente di separare facilmente una serie di valori di ritorno del percorso felice da eventuali errori.

with {:ok, content} <- get_content(),
  :ok <- File.write(path, content) do
    IO.puts "everything worked, happy path code goes here"
else
  # Here we can use a single catch-all failure clause
  # or match every kind of failure individually
  # or match subsets of them however we like
  _some_error => IO.puts "one of those steps failed"
  _other_error => IO.puts "one of those steps failed"
end

Elixir ha ancora funzioni che sollevano eccezioni. Tornando al mio primo esempio, potrei fare uno di questi per sollevare un'eccezione se il file non può essere scritto.

# Raises a generic MatchError because the return value isn't :ok
:ok = File.write(path, content)

# Raises a File.Error with a descriptive error message - eg, saying
# that the file is read-only
File.write!(path, content)

Se io, come chiamante, so che voglio sollevare un errore se la scrittura fallisce, posso scegliere di chiamare File.write!invece di File.write. Oppure posso scegliere di chiamare File.writee gestire in modo diverso ciascuna delle possibili ragioni del fallimento.

Ovviamente è sempre possibile fare rescueun'eccezione se lo vogliamo. Ma rispetto alla gestione di un valore di ritorno informativo, mi sembra imbarazzante. Se so che una chiamata di funzione può fallire o addirittura dovrebbe fallire, il suo fallimento non è un caso eccezionale.

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.