Perché non usare le eccezioni come flusso regolare di controllo?


193

Per evitare tutte le risposte standard su cui avrei potuto cercare su Google, fornirò un esempio che tutti voi potete attaccare a piacimento.

C # e Java (e troppi altri) hanno con molti tipi alcuni comportamenti "a trabocco" che non mi piacciono affatto (ad type.MaxValue + type.SmallestValue == type.MinValueesempio:) int.MaxValue + 1 == int.MinValue.

Ma, vista la mia natura viziosa, aggiungerò un po 'di insulto a questa lesione espandendo questo comportamento a, diciamo un DateTimetipo Overridden . (So ​​che DateTimeè sigillato in .NET, ma per il bene di questo esempio, sto usando un linguaggio pseudo che è esattamente come C #, tranne per il fatto che DateTime non è sigillato).

Il Addmetodo ignorato :

/// <summary>
/// Increments this date with a timespan, but loops when
/// the maximum value for datetime is exceeded.
/// </summary>
/// <param name="ts">The timespan to (try to) add</param>
/// <returns>The Date, incremented with the given timespan. 
/// If DateTime.MaxValue is exceeded, the sum wil 'overflow' and 
/// continue from DateTime.MinValue. 
/// </returns>
public DateTime override Add(TimeSpan ts) 
{
    try
    {                
        return base.Add(ts);
    }
    catch (ArgumentOutOfRangeException nb)
    {
        // calculate how much the MaxValue is exceeded
        // regular program flow
        TimeSpan saldo = ts - (base.MaxValue - this);
        return DateTime.MinValue.Add(saldo)                         
    }
    catch(Exception anyOther) 
    {
        // 'real' exception handling.
    }
}

Naturalmente un if potrebbe risolverlo altrettanto facilmente, ma resta il fatto che non riesco proprio a capire perché non si possano usare le eccezioni (logicamente, posso vedere che quando le prestazioni sono un problema che in alcuni casi le eccezioni dovrebbero essere evitate ).

Penso che in molti casi siano più chiari delle strutture if e non rompano alcun contratto che il metodo sta facendo.

IMHO la reazione "Non usarli mai per un flusso di programma regolare" che tutti sembrano avere non è così ben costruita come la forza di quella reazione può giustificare.

O mi sbaglio?

Ho letto altri post, trattando tutti i tipi di casi speciali, ma il mio punto è che non c'è nulla di sbagliato in questo se entrambi:

  1. Chiaro
  2. Onora il contratto del tuo metodo

Colpiscimi.


2
+1 Mi sento allo stesso modo. Oltre alle prestazioni, l'unica buona ragione per evitare eccezioni per il flusso di controllo è quando il codice chiamante sarà molto più leggibile con i valori restituiti.

4
è: return -1 se è successo qualcosa, return -2 se qualcos'altro, ecc ... davvero più leggibile delle eccezioni?
kender

1
È triste che si ottiene una reputazione negativa per dire la verità: che il tuo esempio non avrebbe potuto essere scritto con dichiarazioni if. (Questo non vuol dire che sia corretto / completo.)
Ingo

8
Direi che lanciare un'eccezione a volte potrebbe essere la tua unica opzione. Ho ad esempio un componente aziendale che inizializza il suo stato interno all'interno del suo costruttore interrogando il database. Ci sono momenti in cui non sono disponibili dati appropriati nel database. Lanciare un'eccezione all'interno del costruttore è l'unico modo per annullare efficacemente la costruzione dell'oggetto. Questo è chiaramente indicato nel contratto (nel mio caso Javadoc) della classe, quindi non ho alcun problema che il codice client potrebbe (e dovrebbe) rilevare tale eccezione durante la creazione del componente e continuare da lì.
Stefan Haberl,

1
Dal momento che hai formulato un'ipotesi, spetta a te citare anche prove / ragioni corroboranti. Per cominciare, cita un motivo per cui il tuo codice è superiore a ifun'istruzione molto più breve e autocompattante . Lo troverai molto difficile. In altre parole: la tua premessa è errata e le conclusioni che ne trai sono quindi sbagliate.
Konrad Rudolph,

Risposte:


171

Hai mai provato a eseguire il debug di un programma generando cinque eccezioni al secondo durante il normale funzionamento?

Io ho.

Il programma era piuttosto complesso (era un server di calcolo distribuito) e una leggera modifica su un lato del programma poteva facilmente rompere qualcosa in un posto completamente diverso.

Vorrei poter semplicemente avviare il programma e attendere che si verifichino eccezioni, ma ci sono state circa 200 eccezioni durante l'avvio nel normale corso delle operazioni

Il mio punto: se usi le eccezioni per situazioni normali, come trovi situazioni insolite (es. Eccezioni )?

Ovviamente, ci sono altri validi motivi per non usare troppo le eccezioni, soprattutto dal punto di vista delle prestazioni


13
Esempio: quando eseguo il debug di un programma .net, lo avvio da Visual Studio e chiedo a VS di interrompere tutte le eccezioni. Se fai affidamento sulle eccezioni come comportamento previsto, non posso più farlo (poiché si spezzerebbe 5 volte al secondo), ed è molto più complicato individuare la parte problematica del codice.
Brann,

15
+1 per sottolineare che non si desidera creare un pagliaio di eccezione in cui trovare un ago eccezionale reale.
Grant Wagner,

16
non ottenere affatto questa risposta, penso che la gente fraintenda qui Non ha nulla a che fare con il debugging, ma con il design. Questo è un ragionamento circolare nella sua forma pura, temo. E il tuo punto è davvero al di là della domanda come affermato in precedenza
Peter,

11
@Peter: Il debug senza rompere le eccezioni è difficile e catturare tutte le eccezioni è doloroso se ce ne sono molti di progettazione. Penso che un design che renda difficile il debug sia quasi parzialmente rotto (in altre parole, il design ha qualcosa a che fare con il debug, IMO)
Brann

7
Anche ignorando il fatto che la maggior parte delle situazioni che voglio eseguire il debug non corrispondono alle eccezioni generate, la risposta alla tua domanda è: "per tipo", ad esempio, dirò al mio debugger di catturare solo AssertionError o StandardError o qualcosa che fa corrisponde a cose brutte che accadono. In caso di problemi, come si fa a fare il log - non si registra per livello e classe, precisamente in modo da poterli filtrare? Pensi che sia anche una cattiva idea?
Ken,

158

Le eccezioni sono sostanzialmente gotodichiarazioni non locali con tutte le conseguenze di quest'ultima. L'uso delle eccezioni per il controllo del flusso viola un principio di minimo stupore , rende i programmi difficili da leggere (ricorda che i programmi sono scritti prima per i programmatori).

Inoltre, questo non è ciò che i fornitori di compilatori si aspettano. Si aspettano che le eccezioni vengano generate raramente e di solito lasciano il throwcodice abbastanza inefficiente. Generare eccezioni è una delle operazioni più costose in .NET.

Tuttavia, alcuni linguaggi (in particolare Python) usano le eccezioni come costrutti di controllo del flusso. Ad esempio, gli iteratori sollevano StopIterationun'eccezione se non ci sono altri elementi. Anche i costrutti di linguaggio standard (come for) si basano su questo.


11
ehi, le eccezioni non sono sorprendenti! E ti contraddici un po 'quando dici "è una cattiva idea" e poi continui a dire "ma è una buona idea in pitone".
hasen

6
Non sono ancora affatto convinto: 1) L'efficienza era oltre alla domanda, molti programmi non bacht non potevano fregarsene di meno (ad es. L'interfaccia utente) 2) Sorprendente: come ho detto, è sorprendente perché non viene usato, ma la domanda rimane: perché non usare id in primo luogo? Ma, poiché questa è la risposta
Peter,

4
+1 In realtà sono contento che tu abbia sottolineato la distinzione tra Python e C #. Non penso sia una contraddizione. Python è molto più dinamico e l'aspettativa di usare le eccezioni in questo modo è inserita nel linguaggio. Fa anche parte della cultura EAFP di Python. Non so quale approccio sia concettualmente più puro o più coerente, ma mi piace l'idea di scrivere codice che faccia quello che gli altri si aspettano che faccia, il che significa stili diversi in lingue diverse.
Mark E. Haase,

13
A differenza goto, ovviamente, delle eccezioni interagiscono correttamente con lo stack di chiamate e con l'ambito lessicale e non lasciare lo stack o gli ambiti in un pasticcio.
Lukas Eder, l'

4
In realtà, la maggior parte dei distributori di macchine virtuali si aspetta eccezioni e le gestisce in modo efficiente. Come fa notare @LukasEder, le eccezioni sono completamente diverse da goto in quanto strutturate.
Marcin,

30

La mia regola empirica è:

  • Se riesci a fare qualsiasi cosa per recuperare da un errore, rileva le eccezioni
  • Se l'errore è molto comune (ad es. L'utente ha tentato di accedere con una password errata), utilizzare i valori di ritorno
  • Se non riesci a fare nulla per recuperare da un errore, lascialo irrisolto (o intercettalo nel ricevitore principale per eseguire un arresto semi-grazioso dell'applicazione)

Il problema che vedo con le eccezioni è da un punto di vista puramente sintattico (sono abbastanza sicuro che il sovraccarico delle prestazioni sia minimo). Non mi piacciono i blocchi di prova ovunque.

Prendi questo esempio:

try
{
   DoSomeMethod();  //Can throw Exception1
   DoSomeOtherMethod();  //Can throw Exception1 and Exception2
}
catch(Exception1)
{
   //Okay something messed up, but is it SomeMethod or SomeOtherMethod?
}

.. Un altro esempio potrebbe essere quando è necessario assegnare qualcosa a un handle utilizzando una factory e tale factory potrebbe generare un'eccezione:

Class1 myInstance;
try
{
   myInstance = Class1Factory.Build();
}
catch(SomeException)
{
   // Couldn't instantiate class, do something else..
}
myInstance.BestMethodEver();   // Will throw a compile-time error, saying that myInstance is uninitalized, which it potentially is.. :(

Quindi, personalmente, penso che dovresti mantenere le eccezioni per rare condizioni di errore (memoria insufficiente, ecc.) E utilizzare valori di ritorno (valori, strutture o enumerazioni) per fare il controllo degli errori.

Spero di aver capito bene la tua domanda :)


4
ri: Il tuo secondo esempio - perché non inserire la chiamata a BestMethodEver all'interno del blocco try, dopo Build? Se Build () genera un'eccezione, non verrà eseguita e il compilatore sarà felice.
Blorgbeard esce l'

2
Sì, sarebbe probabilmente quello con cui finirai, ma considera un esempio più complesso in cui il tipo myInstance stesso può generare eccezioni. E anche altre istanze nell'ambito del metodo possono farlo.
Finirai

È necessario eseguire la traduzione delle eccezioni (in un tipo di eccezione adeguato al livello di astrazione) nel blocco catch. FYI: "Multi-catch" dovrebbe entrare in Java 7.
jasonnerothin

FYI: In C ++, puoi inserire più catture dopo aver provato a catturare diverse eccezioni.
RobH

2
Per il software di termoretraibile, è necessario rilevare tutte le eccezioni. Almeno apri una finestra di dialogo che spieghi che il programma deve essere chiuso, ed ecco qualcosa di incomprensibile che puoi inviare in una segnalazione di bug.
David Thornley,

25

Una prima reazione a molte risposte:

stai scrivendo per i programmatori e il principio del minimo stupore

Ovviamente! Ma un se non è più chiaro per tutto il tempo.

Non dovrebbe essere sorprendente, ad esempio: divide (1 / x) catch (divisionByZero) è più chiaro di qualsiasi altro se per me (a Conrad e altri). Il fatto che questo tipo di programmazione non sia previsto è puramente convenzionale e, in effetti, ancora rilevante. Forse nel mio esempio un if sarebbe più chiaro.

Ma DivisionByZero e FileNotFound del resto sono più chiari degli if.

Naturalmente se è meno performante e ha bisogno di un tempo di miliardi di secondi al secondo, dovresti ovviamente evitarlo, ma non ho ancora letto alcun buon motivo per evitare il disegno generale.

Per quanto riguarda il principio del minimo stupore: esiste il pericolo del ragionamento circolare qui: supponiamo che un'intera comunità usi un cattivo design, questo design ci si aspetta! Pertanto, il principio non può essere un graal e dovrebbe essere considerato attentamente.

eccezioni per situazioni normali, come individuare situazioni insolite (cioè eccezionali)?

In molte reazioni sth. come questo brilla attraverso. Prendili e basta, no? Il tuo metodo dovrebbe essere chiaro, ben documentato e sfoggiare il suo contratto. Non capisco quella domanda che devo ammettere.

Debug su tutte le eccezioni: lo stesso, a volte viene fatto solo perché il design non usa eccezioni è comune. La mia domanda era: perché è comune in primo luogo?


1
1) Controlli sempre xprima di chiamare 1/x? 2) Avvolgi ogni operazione di divisione in un blocco try-catch da catturare DivideByZeroException? 3) Quale logica metti nel blocco catch da cui recuperare DivideByZeroException?
Lightman,

1
Tranne DivisionByZero e FileNotFound sono cattivi esempi poiché sono casi eccezionali che dovrebbero essere trattati come eccezioni.
0x6C38,

Non c'è nulla di "eccezionalmente eccezionale" in un file non trovato nel modo in cui le persone "anti-eccezione" qui propagandano. openConfigFile();può essere seguito da un FileNotFound catturato con { createDefaultConfigFile(); setFirstAppRun(); }un'eccezione FileNotFound gestita con garbo; nessun crash, rendiamo l'esperienza dell'utente finale migliore, non peggiore. Potresti dire "Ma cosa succede se questa non è davvero la prima manche e la ottengono ogni volta?" Almeno l'app viene eseguita ogni volta e non si arresta in modo anomalo ad ogni avvio! Su un 1 a 10 "questo è terribile": "prima esecuzione" ogni avvio = 3 o 4, arresto anomalo ogni avvio = 10.
Loduwijk

I tuoi esempi sono eccezioni. No, non si controlla sempre xprima di chiamare 1/x, perché di solito va bene. Il caso eccezionale è il caso in cui non va bene. Qui non stiamo parlando di sconvolgimento della terra, ma per esempio per un numero intero di base dato un casuale x, solo 1 su 4294967296 non riuscirebbe a fare la divisione. È eccezionale e le eccezioni sono un buon modo per affrontarlo. Tuttavia, potresti usare le eccezioni per implementare l'equivalente di switchun'affermazione, ma sarebbe piuttosto sciocco.
Thor84no,

16

Prima delle eccezioni, in C, c'erano setjmpe longjmpche potevano essere usati per realizzare un simile srotolamento del telaio dello stack.

Quindi allo stesso costrutto è stato assegnato un nome: "Eccezione". E la maggior parte delle risposte si basa sul significato di questo nome per discutere del suo utilizzo, sostenendo che le eccezioni sono intese per essere utilizzate in condizioni eccezionali. Questo non era mai l'intento nell'originale longjmp. C'erano solo situazioni in cui era necessario interrompere il flusso di controllo in molti frame dello stack.

Le eccezioni sono leggermente più generali in quanto è possibile utilizzarle anche all'interno dello stesso frame dello stack. Ciò solleva analogie con gotoquelle che ritengo errate. Le goto sono una coppia strettamente accoppiata (e così sono setjmpe longjmp). Le eccezioni seguono una pubblicazione / sottoscrizione liberamente accoppiata che è molto più pulita! Quindi usarli all'interno dello stesso frame dello stack non è quasi la stessa cosa che usare gotos.

La terza fonte di confusione riguarda se sono state verificate o non controllate le eccezioni. Naturalmente, le eccezioni non controllate sembrano particolarmente terribili da usare per il flusso di controllo e forse molte altre cose.

Le eccezioni verificate sono comunque ottime per il flusso di controllo, una volta superate tutte le interruzioni vittoriane e vivendo un po '.

Il mio utilizzo preferito è una sequenza di throw new Success()un lungo frammento di codice che prova una cosa dopo l'altra fino a trovare quello che sta cercando. Ogni cosa - ogni pezzo di logica - può avere un annidamento arbritrario, quindi breaksono fuori come anche qualsiasi tipo di test delle condizioni. Lo if-elseschema è fragile. Se modifico elseo incasino la sintassi in qualche altro modo, allora c'è un bug peloso.

L'uso throw new Success() linearizza il flusso del codice. Uso Successclassi definite localmente - controllate ovviamente - in modo che se dimentico di prenderlo il codice non si compili. E non prendo Successes altro metodo .

A volte il mio codice controlla una cosa dopo l'altra e riesce solo se tutto è OK. In questo caso ho una linearizzazione simile usando throw new Failure().

L'uso di una funzione separata crea problemi con il livello naturale di compartimentazione. Quindi la returnsoluzione non è ottimale. Preferisco avere una o due pagine di codice in un posto per ragioni cognitive. Non credo nel codice diviso in modo ultra fine.

Quello che fanno le JVM o i compilatori è meno rilevante per me a meno che non ci sia un hotspot. Non riesco a credere che ci sia una ragione fondamentale per i compilatori di non rilevare eccezioni lanciate e catturate localmente e semplicemente trattarle come messaggi molto efficienti gotoa livello di codice macchina.

Per quanto riguarda il loro utilizzo attraverso le funzioni per il flusso di controllo - vale a dire per casi comuni piuttosto che eccezionali - non riesco a vedere come sarebbero meno efficienti di più interruzioni, test di condizione, ritorni a guadare attraverso tre frame dello stack invece di ripristinare il puntatore dello stack.

Personalmente non utilizzo il modello attraverso i frame dello stack e posso vedere come sarebbe necessario un design sofisticato per farlo in modo elegante. Ma usato con parsimonia dovrebbe andare bene.

Infine, per quanto riguarda i programmatori vergini sorprendenti, non è un motivo convincente. Se li introduci delicatamente alla pratica, impareranno ad amarla. Ricordo che C ++ era solito sorprendere e spaventare i programmatori C.


3
Usando questo schema, la maggior parte delle mie funzioni grossolane hanno due piccoli problemi alla fine: uno per Successo e uno per Fallimento ed è lì che la funzione avvolge cose come preparare la risposta servlet corretta o preparare valori di ritorno. Avere un unico posto per concludere è bello. L' returnalternativa -pattern richiederebbe due funzioni per ciascuna di tali funzioni. Uno esterno per preparare la risposta servlet o altre azioni simili e uno interno per eseguire il calcolo. PS: Un professore inglese probabilmente suggerirebbe di usare "sorprendente" piuttosto che "sorprendente" nell'ultimo paragrafo :-)
Negromante

11

La risposta standard è che le eccezioni non sono regolari e dovrebbero essere utilizzate in casi eccezionali.

Uno dei motivi, per me importante, è che quando leggo una try-catchstruttura di controllo in un software che mantengo o eseguo il debug, provo a scoprire perché il programmatore originale ha usato una gestione delle eccezioni anziché una if-elsestruttura. E mi aspetto di trovare una buona risposta.

Ricorda che scrivi il codice non solo per il computer ma anche per altri programmatori. C'è un semantico associato a un gestore di eccezioni che non puoi buttare via solo perché alla macchina non importa.


Una risposta sottovalutata credo. Il computer potrebbe non rallentare molto quando trova un'eccezione che viene ingoiata, ma quando sto lavorando sul codice di qualcun altro e mi imbatto in esso mi ferma nelle mie tracce mentre mi alleno se ho perso qualcosa di importante che non ho sapete, o se in realtà non ci sono giustificazioni per l'uso di questo anti-pattern.
Tim Abell,

9

Che ne dici di prestazioni? Durante il test di caricamento di un'app Web .NET abbiamo superato 100 utenti simulati per server Web fino a quando non abbiamo risolto un'eccezione che si verifica comunemente e quel numero è aumentato a 500 utenti.


8

Josh Bloch affronta ampiamente questo argomento in Effective Java. I suoi suggerimenti sono illuminanti e dovrebbero applicarsi anche a .Net (ad eccezione dei dettagli).

In particolare, le eccezioni dovrebbero essere utilizzate per circostanze eccezionali. Le ragioni sono principalmente legate all'usabilità. Affinché un determinato metodo sia massimamente utilizzabile, le sue condizioni di input e output dovrebbero essere limitate al massimo.

Ad esempio, il secondo metodo è più facile da usare rispetto al primo:

/**
 * Adds two positive numbers.
 *
 * @param addend1 greater than zero
 * @param addend2 greater than zero
 * @throws AdditionException if addend1 or addend2 is less than or equal to zero
 */
int addPositiveNumbers(int addend1, int addend2) throws AdditionException{
  if( addend1 <= 0 ){
     throw new AdditionException("addend1 is <= 0");
  }
  else if( addend2 <= 0 ){
     throw new AdditionException("addend2 is <= 0");
  }
  return addend1 + addend2;
}

/**
 * Adds two positive numbers.
 *
 * @param addend1 greater than zero
 * @param addend2 greater than zero
 */
public int addPositiveNumbers(int addend1, int addend2) {
  if( addend1 <= 0 ){
     throw new IllegalArgumentException("addend1 is <= 0");
  }
  else if( addend2 <= 0 ){
     throw new IllegalArgumentException("addend2 is <= 0");
  }
  return addend1 + addend2;
}

In entrambi i casi, è necessario verificare per assicurarsi che il chiamante stia utilizzando l'API in modo appropriato. Ma nel secondo caso, è necessario (implicitamente). Le eccezioni soft verranno comunque generate se l'utente non ha letto il javadoc, ma:

  1. Non è necessario documentarlo.
  2. Non è necessario testarlo (a seconda di quanto sia aggressiva la strategia di test dell'unità).
  3. Non è necessario che il chiamante gestisca tre casi d'uso.

Il punto fondamentale è che le eccezioni non devono essere utilizzate come codici di ritorno, soprattutto perché hai complicato non solo la TUA API, ma anche l'API del chiamante.

Fare la cosa giusta ha un costo, ovviamente. Il costo è che tutti devono capire che devono leggere e seguire la documentazione. Spero che sia così comunque.


7

Penso che puoi usare Eccezioni per il controllo del flusso. C'è, tuttavia, un rovescio della medaglia di questa tecnica. La creazione di eccezioni è una cosa costosa, perché devono creare una traccia dello stack. Quindi, se si desidera utilizzare le eccezioni più spesso rispetto alla semplice segnalazione di una situazione eccezionale, è necessario assicurarsi che la creazione delle tracce dello stack non influisca negativamente sulle prestazioni.

Il modo migliore per ridurre i costi di creazione delle eccezioni è quello di sovrascrivere il metodo fillInStackTrace () in questo modo:

public Throwable fillInStackTrace() { return this; }

Tale eccezione non avrà compilato stacktraces.


StackTrace richiede inoltre al chiamante di "conoscere" (cioè avere una dipendenza da) tutti i Throwables nello stack. Questa è una brutta cosa Generare eccezioni appropriate al livello di astrazione (ServiceExceptions in Services, DaoExceptions from Dao metodi, ecc.). Basta tradurre se necessario.
Jasonnerothin,

5

Non vedo davvero come controlli il flusso del programma nel codice che hai citato. Non vedrai mai un'altra eccezione oltre all'eccezione ArgumentOutOfRange. (Quindi la tua seconda clausola di cattura non verrà mai colpita). Tutto quello che stai facendo è usare un lancio estremamente costoso per imitare un'istruzione if.

Inoltre non stai eseguendo le operazioni più sinistre in cui lanci un'eccezione puramente per essere catturato da qualche altra parte per eseguire il controllo del flusso. Stai effettivamente gestendo un caso eccezionale.


5

Ecco le migliori pratiche che ho descritto nel mio post sul blog :

  • Lancia un'eccezione per indicare una situazione inaspettata nel tuo software.
  • Utilizzare i valori di ritorno per la convalida dell'input .
  • Se sai come gestire le eccezioni generate da una libreria, prendile al livello più basso possibile .
  • Se si riscontra un'eccezione imprevista, eliminare completamente l'operazione corrente. Non far finta di sapere come gestirli .

4

Poiché il codice è difficile da leggere, è possibile che si verifichino problemi durante il debug, si introdurranno nuovi bug quando si correggono i bug dopo molto tempo, è più costoso in termini di risorse e tempo e ti dà fastidio se si esegue il debug del codice e il debugger si interrompe al verificarsi di ogni eccezione;)


4

A parte i motivi indicati, uno dei motivi per non utilizzare le eccezioni per il controllo del flusso è che può complicare notevolmente il processo di debug.

Ad esempio, quando sto cercando di rintracciare un bug in VS, in genere accendo "Rompi tutte le eccezioni". Se stai usando le eccezioni per il controllo del flusso, interromperò regolarmente il debugger e dovrò continuare a ignorare queste eccezioni non eccezionali fino a quando non avrò il vero problema. Questo probabilmente farà impazzire qualcuno !!


1
Ho già tenuto in mano quell'unico: Debug su tutte le eccezioni: lo stesso, è stato fatto solo perché il design di non usare le eccezioni è comune. La mia domanda era: perché è comune in primo luogo?
Peter,

Quindi la tua risposta è sostanzialmente "È un male perché Visual Studio ha questa funzionalità ..."? Ho programmato per circa 20 anni e non ho nemmeno notato che c'era un'opzione "rompere su tutte le eccezioni". Tuttavia, "grazie a questa caratteristica!" sembra una ragione debole. Basta tracciare l'eccezione alla sua fonte; speriamo che tu stia usando un linguaggio che lo renda facile, altrimenti il ​​tuo problema è con le caratteristiche del linguaggio, non con l'uso generale delle eccezioni stesse.
Loduwijk,

3

Supponiamo che tu abbia un metodo che esegue alcuni calcoli. Ci sono molti parametri di input che deve convalidare, quindi per restituire un numero maggiore di 0.

L'uso dei valori di ritorno per segnalare un errore di convalida è semplice: se il metodo ha restituito un numero inferiore a 0, si è verificato un errore. Come dire quindi quale parametro non è stato validato?

Ricordo dai miei giorni in C molte funzioni hanno restituito codici di errore come questo:

-1 - x lesser then MinX
-2 - x greater then MaxX
-3 - y lesser then MinY

eccetera.

È davvero meno leggibile quindi lanciare e catturare un'eccezione?


ecco perché hanno inventato gli enum :) Ma i numeri magici sono un argomento completamente diverso .. en.wikipedia.org/wiki/…
Isak Savo

Ottimo esempio Stavo per scrivere la stessa cosa. @IsakSavo: gli enumeratori non sono utili in questa situazione se si prevede che il metodo restituisca un valore o un oggetto di significato. Ad esempio getAccountBalance () dovrebbe restituire un oggetto Money, non un oggetto AccountBalanceResultEnum. Molti programmi C hanno un modello simile in cui un valore sentinella (0 o null) rappresenta un errore, quindi è necessario chiamare un'altra funzione per ottenere un codice di errore separato al fine di determinare il motivo dell'errore. (L'API MySQL C è così.)
Mark E. Haase,

3

Puoi usare un artiglio di martello per girare una vite, proprio come puoi usare le eccezioni per il flusso di controllo. Ciò non significa che sia l' uso previsto della funzione. La ifdichiarazione esprime condizioni, il cui uso previsto è il controllo del flusso.

Se si utilizza una funzione in modo non intenzionale mentre si sceglie di non utilizzare la funzionalità progettata a tale scopo, ci sarà un costo associato. In questo caso, la chiarezza e le prestazioni soffrono senza un reale valore aggiunto. Cosa ti consente di utilizzare le eccezioni rispetto alla ifdichiarazione ampiamente accettata ?

Detto in altro modo: solo perché si può non significa che si dovrebbe .


1
Stai dicendo che non c'è bisogno di eccezioni, dopo che abbiamo ottenuto ifun uso normale o l'uso delle esecuzioni non è inteso perché non è inteso (argomento circolare)?
Val

1
@Val: le eccezioni sono per situazioni eccezionali - se rileviamo abbastanza per lanciare un'eccezione e gestirla, abbiamo abbastanza informazioni per non lanciarla e comunque gestirla. Possiamo andare direttamente alla logica di gestione e saltare il tentativo / cattura costoso e superfluo.
Bryan Watts,

Secondo questa logica, potresti anche non avere eccezioni e fare sempre un'uscita di sistema invece di generare un'eccezione. Se vuoi fare qualcosa prima di uscire, crea un wrapper e chiamalo. Esempio Java: public class ExitHelper{ public static void cleanExit() { cleanup(); System.exit(1); } }Quindi chiamalo invece di lanciare: ExitHelper.cleanExit();se il tuo argomento fosse valido, questo sarebbe l'approccio preferito e non ci sarebbero eccezioni. In pratica stai dicendo "L'unica ragione per le eccezioni è quella di bloccarsi in modo diverso".
Loduwijk,

@Aaron: se entrambi lancio e prendo l'eccezione, ho abbastanza informazioni per evitare di farlo. Ciò non significa che tutte le eccezioni siano improvvisamente fatali. Un altro codice che non controllo potrebbe catturarlo, e va bene. La mia tesi, che rimane solida, è che lanciare e catturare un'eccezione nello stesso contesto sia superfluo. Non ho dichiarato, né vorrei dire, che tutte le eccezioni dovrebbero uscire dal processo.
Bryan Watts,

@BryanWatts riconosciuto. Molti altri hanno detto che dovresti usare le eccezioni solo per tutto ciò che non riesci a recuperare e, per estensione, dovrebbero sempre andare in crash sulle eccezioni. Questo è il motivo per cui è difficile discutere di queste cose; non ci sono solo 2 opinioni, ma molte. Non sono ancora d'accordo con te, ma non fortemente. Ci sono momenti in cui lanciare / catturare insieme è il codice più leggibile, gestibile e più bello; di solito questo accade se stai già rilevando altre eccezioni, quindi hai già provato / catturato e l'aggiunta di 1 o 2 catture in più è più pulita dei controlli di errore separati da if.
Loduwijk,

3

Come altri hanno già accennato numerose volte, il principio del minimo stupore vieta l'uso eccessivo delle eccezioni solo per scopi di controllo del flusso. D'altra parte, nessuna regola è corretta al 100%, e ci sono sempre quei casi in cui un'eccezione è "lo strumento giusto" - molto simile a gotose stessa, a proposito, che viene fornita sotto forma di breake continuein linguaggi come Java, che sono spesso il modo perfetto per saltare da anelli pesantemente annidati, che non sono sempre evitabili.

Il seguente post sul blog spiega un caso d'uso piuttosto complesso ma anche piuttosto interessante per un non locale ControlFlowException :

Spiega come all'interno di jOOQ (una libreria di astrazione SQL per Java) , tali eccezioni vengono occasionalmente utilizzate per interrompere presto il processo di rendering SQL quando viene soddisfatta una condizione "rara".

Esempi di tali condizioni sono:

  • Sono stati rilevati troppi valori di bind. Alcuni database non supportano numeri arbitrari di valori di bind nelle loro istruzioni SQL (SQLite: 999, Ingres 10.1.0: 1024, Sybase ASE 15.5: 2000, SQL Server 2008: 2100). In questi casi, jOOQ interrompe la fase di rendering SQL e esegue nuovamente il rendering dell'istruzione SQL con valori di bind incorporati. Esempio:

    // Pseudo-code attaching a "handler" that will
    // abort query rendering once the maximum number
    // of bind values was exceeded:
    context.attachBindValueCounter();
    String sql;
    try {
    
      // In most cases, this will succeed:
      sql = query.render();
    }
    catch (ReRenderWithInlinedVariables e) {
      sql = query.renderWithInlinedBindValues();
    }

    Se estraessimo esplicitamente i valori di bind dalla query AST per contarli ogni volta, sprecheremmo preziosi cicli della CPU per il 99,9% delle query che non soffrono di questo problema.

  • Alcune logiche sono disponibili solo indirettamente tramite un'API che vogliamo eseguire solo "parzialmente". Il UpdatableRecord.store()metodo genera un'istruzione INSERTo UPDATE, a seconda Recorddei flag interni del. Dall'esterno, non sappiamo in quale tipo di logica sia contenuta store()(ad es. Blocco ottimistico, gestione del listener di eventi, ecc.), Quindi non vogliamo ripetere quella logica quando memorizziamo diversi record in un'istruzione batch, dove vorremmo store()solo generare l'istruzione SQL, non effettivamente eseguirla. Esempio:

    // Pseudo-code attaching a "handler" that will
    // prevent query execution and throw exceptions
    // instead:
    context.attachQueryCollector();
    
    // Collect the SQL for every store operation
    for (int i = 0; i < records.length; i++) {
      try {
        records[i].store();
      }
    
      // The attached handler will result in this
      // exception being thrown rather than actually
      // storing records to the database
      catch (QueryCollectorException e) {
    
        // The exception is thrown after the rendered
        // SQL statement is available
        queries.add(e.query());                
      }
    }

    Se avessimo esternalizzato la store()logica in API "riutilizzabili" che possono essere personalizzate per facoltativamente non eseguire l'SQL, cercheremmo di creare un'API piuttosto difficile da mantenere, difficilmente riutilizzabile.

Conclusione

In sostanza, il nostro uso di questi non locali gotoè proprio in linea con quanto affermato da [Mason Wheeler] [5] nella sua risposta:

"Ho appena incontrato una situazione che non posso affrontare correttamente a questo punto, perché non ho abbastanza contesto per gestirlo, ma la routine che mi ha chiamato (o qualcosa più in alto nello stack di chiamate) dovrebbe sapere come gestirlo ".

Entrambi gli usi ControlFlowExceptionserano piuttosto facili da implementare rispetto alle loro alternative, permettendoci di riutilizzare una vasta gamma di logiche senza refactoring dai relativi interni.

Ma la sensazione di essere un po 'una sorpresa per i futuri manutentori rimane. Il codice sembra piuttosto delicato e, sebbene in questo caso sia stata la scelta giusta, preferiremmo sempre non utilizzare le eccezioni per il flusso di controllo locale , dove è facile evitare l'uso di diramazioni ordinarie if - else.


2

In genere non c'è nulla di sbagliato, di per sé, nel gestire un'eccezione a un livello basso. Un'eccezione È un messaggio valido che fornisce molti dettagli sul perché un'operazione non può essere eseguita. E se riesci a gestirlo, dovresti.

In generale, se sai che esiste un'alta probabilità di fallimento che puoi verificare per ... dovresti fare il controllo ... cioè se (obj! = Null) obj.method ()

Nel tuo caso, non ho abbastanza familiarità con la libreria C # per sapere se l'ora della data ha un modo semplice per verificare se un timestamp è fuori limite. In tal caso, chiama if (.isvalid (ts)), altrimenti il ​​tuo codice andrà sostanzialmente bene.

Quindi, in sostanza, si riduce a qualsiasi modo crei un codice più pulito ... se l'operazione di protezione contro un'eccezione prevista è più complessa della semplice gestione dell'eccezione; di quanto tu abbia il mio permesso di gestire l'eccezione invece di creare guardie complesse ovunque.


Punto aggiuntivo: se la tua eccezione fornisce informazioni sull'acquisizione degli errori (un getter come "Param getWhatParamMessedMeUp ()"), può aiutare l'utente dell'API a prendere una buona decisione su cosa fare dopo. Altrimenti, stai solo dando un nome a uno stato di errore.
Jasonnerothin,

2

Se si utilizzano gestori di eccezioni per il flusso di controllo, si è troppo generici e pigri. Come ha detto qualcun altro, sai che è successo qualcosa se stai gestendo l'elaborazione nel gestore, ma cosa esattamente? Sostanzialmente si utilizza l'eccezione per un'istruzione else, se la si utilizza per il flusso di controllo.

Se non sai quale possibile stato potrebbe verificarsi, puoi utilizzare un gestore di eccezioni per stati imprevisti, ad esempio quando devi utilizzare una libreria di terze parti o devi catturare tutto nell'interfaccia utente per mostrare un bel errore messaggio e registra l'eccezione.

Tuttavia, se sai cosa potrebbe andare storto e non metti un'istruzione if o qualcosa per verificarlo, allora sei solo pigro. Consentire al gestore delle eccezioni di essere il tuttofare per le cose che sai che potrebbero accadere è pigro e tornerà a perseguitarti più tardi, perché proverai a risolvere una situazione nel gestore delle eccezioni sulla base di un'ipotesi forse falsa.

Se metti la logica nel gestore delle eccezioni per determinare cosa è successo esattamente, allora saresti abbastanza stupido per non inserire quella logica nel blocco try.

I gestori delle eccezioni sono l'ultima risorsa, perché quando finisci le idee / i modi per impedire che qualcosa vada storto, o le cose vanno oltre la tua capacità di controllo. Ad esempio, il server è inattivo e in timeout e non puoi impedire che venga generata l'eccezione.

Infine, avere tutti i controlli effettuati in anticipo mostra ciò che sai o ti aspetti che accada e lo rende esplicito. Il codice dovrebbe essere chiaro nelle intenzioni. Cosa preferiresti leggere?


1
Non è affatto vero: "Essenzialmente stai usando l'eccezione per un'istruzione else, se la stai usando per il flusso di controllo." Se la usi per il flusso di controllo, sai esattamente cosa catturi e non usi mai una cattura generale, ma un uno specifico ovviamente!
Peter,

2

Potresti essere interessato a dare un'occhiata al sistema di condizioni di Common Lisp che è una sorta di generalizzazione delle eccezioni fatta bene. Poiché puoi rilassare la pila o meno in modo controllato, ottieni anche "riavvii", che sono estremamente utili.

Questo non ha molto a che fare con le migliori pratiche in altre lingue, ma ti mostra cosa si può fare con un pensiero progettuale (approssimativamente) nella direzione in cui stai pensando.

Ovviamente ci sono ancora considerazioni sulle prestazioni se rimbalzi su e giù per lo stack come uno yo-yo, ma è un'idea molto più generale del tipo di approccio "oh schifo, lascia cauzione" che la maggior parte dei sistemi di eccezione cattura / getta.


2

Non credo che ci sia qualcosa di sbagliato nell'usare Eccezioni per il controllo del flusso. Le eccezioni sono in qualche modo simili alle continuazioni e nei linguaggi tipicamente statici, le Eccezioni sono più potenti delle continuazioni, quindi, se hai bisogno di continuazioni ma la tua lingua non le ha, puoi usare Eccezioni per implementarle.

Bene, in realtà, se hai bisogno di continuazioni e la tua lingua non le ha, hai scelto la lingua sbagliata e dovresti piuttosto usarne una diversa. Ma a volte non hai scelta: la programmazione web sul lato client è il primo esempio: non c'è modo di aggirare JavaScript.

Un esempio: Microsoft Volta è un progetto che consente di scrivere applicazioni Web in .NET diretto e che il framework si occupi di capire quali bit devono essere eseguiti dove. Una conseguenza di ciò è che Volta deve essere in grado di compilare CIL in JavaScript, in modo da poter eseguire il codice sul client. Tuttavia, c'è un problema: .NET ha il multithreading, JavaScript no. Quindi, Volta implementa le continuazioni in JavaScript usando JavaScript Exceptions, quindi implementa i thread .NET usando quelle continuazioni. In questo modo, le applicazioni Volta che utilizzano i thread possono essere compilate per essere eseguite in un browser non modificato, senza bisogno di Silverlight.


2

Un motivo estetico:

Un tentativo arriva sempre con una cattura, mentre un if non deve venire con un altro.

if (PerformCheckSucceeded())
   DoSomething();

Con try / catch, diventa molto più dettagliato.

try
{
   PerformCheckSucceeded();
   DoSomething();
}
catch
{
}

Sono troppe 6 righe di codice.


1

Ma non saprai sempre cosa succede nel Metodo / i che chiami. Non saprai esattamente dove è stata generata l'eccezione. Senza esaminare l'oggetto dell'eccezione in modo più dettagliato ....


1

Sento che non c'è niente di sbagliato nel tuo esempio. Al contrario, sarebbe un peccato ignorare l'eccezione generata dalla funzione chiamata.

Nella JVM, lanciare un'eccezione non è poi così costoso, creando l'eccezione solo con la nuova xyzException (...), poiché quest'ultima comporta una camminata dello stack. Quindi, se hai creato alcune eccezioni in anticipo, puoi lanciarle molte volte senza costi. Naturalmente, in questo modo non è possibile trasmettere dati insieme all'eccezione, ma penso che sia comunque una brutta cosa da fare.


Mi dispiace, è assolutamente sbagliato, Brann. Dipende dalla condizione. Non è sempre il caso che la condizione sia banale. Pertanto, un'istruzione if potrebbe richiedere ore o giorni o anche di più.
Ingo

Nella JVM, cioè. Non più costoso di un ritorno. Vai a capire. Ma la domanda è: cosa scriveresti nell'istruzione if, se non proprio il codice che è già presente nella funzione chiamata per distinguere un caso eccezionale da un normale --- quindi la duplicazione del codice.
Ingo

1
Ingo: una situazione eccezionale è quella che non ti aspetti. cioè uno a cui non hai pensato come programmatore. Quindi la mia regola è "scrivere codice che non generi eccezioni" :)
Brann

1
Non scrivo mai gestori di eccezioni, risolvo sempre il problema (tranne quando non posso farlo perché non ho il controllo sul codice di errore). E non lancio mai eccezioni, tranne quando il codice che ho scritto è destinato a qualcun altro da usare (ad esempio una libreria). No mostrami la contraddizione?
Brann,

1
Sono d'accordo con te a non gettare eccezioni selvaggiamente. Ma certamente, ciò che è "eccezionale" è una questione di definizione. Quel String.parseDouble genera un'eccezione se non è in grado di fornire un risultato utile, ad esempio, è in ordine. Cos'altro dovrebbe fare? Ritorno NaN? Che dire dell'hardware non IEEE?
Ingo,

1

Esistono alcuni meccanismi generali attraverso i quali una lingua potrebbe consentire l'uscita di un metodo senza restituire un valore e distendersi al successivo blocco "catch":

  • Chiedi al metodo di esaminare il frame dello stack per determinare il sito di chiamata e utilizzare i metadati per il sito di chiamata per trovare informazioni su un tryblocco all'interno del metodo di chiamata o la posizione in cui il metodo di chiamata ha memorizzato l'indirizzo del suo chiamante; in quest'ultima situazione, esaminare i metadati che il chiamante determina allo stesso modo del chiamante immediato, ripetendo fino a quando non si trova un tryblocco o lo stack è vuoto. Questo approccio aggiunge pochissime spese generali al caso senza eccezioni (impedisce alcune ottimizzazioni) ma è costoso quando si verifica un'eccezione.

  • Chiedi al metodo di restituire un flag "nascosto" che distingue un normale ritorno da un'eccezione e chiedi al chiamante di controllare quel flag e il ramo in una routine "eccezione" se è impostato. Questa routine aggiunge 1-2 istruzioni al caso senza eccezioni, ma un sovraccarico relativamente piccolo quando si verifica un'eccezione.

  • Chiedere al chiamante di inserire informazioni o codice di gestione delle eccezioni a un indirizzo fisso relativo all'indirizzo di ritorno in pila. Ad esempio, con ARM, invece di utilizzare l'istruzione "BL subroutine", è possibile utilizzare la sequenza:

        adr lr,next_instr
        b subroutine
        b handle_exception
    next_instr:
    

Per uscire normalmente, la subroutine semplicemente farebbe bx lro pop {pc}; in caso di uscita anomala, la subroutine sottrarrebbe 4 da LR prima di eseguire il ritorno o l'uso sub lr,#4,pc(a seconda della variazione ARM, modalità di esecuzione, ecc.) Questo approccio non funzionerà molto male se il chiamante non è progettato per adattarlo.

Un linguaggio o un framework che utilizza le eccezioni verificate potrebbe trarre vantaggio dall'avere quelle gestite con un meccanismo come il n. 2 o il n. 3 sopra, mentre le eccezioni non controllate sono gestite utilizzando il n. 1. Sebbene l'implementazione delle eccezioni verificate in Java sia piuttosto fastidiosa, non sarebbero un cattivo concetto se esistesse un modo in cui un sito di chiamata potrebbe dire, in sostanza, "Questo metodo è dichiarato come lancio XX, ma non me lo aspetto mai farlo; in tal caso, riproporlo come un'eccezione "non selezionata". In un quadro in cui le eccezioni controllate sono state gestite in questo modo, potrebbero essere un mezzo efficace di controllo del flusso per cose come i metodi di analisi che in alcuni contesti potrebbero avere un elevata probabilità di fallimento, ma in cui il fallimento dovrebbe restituire informazioni sostanzialmente diverse rispetto al successo. non sono a conoscenza di alcun framework che utilizza tale schema, tuttavia. Invece, il modello più comune è utilizzare il primo approccio sopra (costo minimo per il caso senza eccezioni, ma costo elevato quando vengono generate eccezioni) per tutte le eccezioni.

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.