Considerazioni sulla gestione degli errori


31

Il problema:

Da molto tempo, sono preoccupato per il exceptionsmeccanismo, perché sento che non risolve davvero ciò che dovrebbe.

RECLAMO: Ci sono lunghi dibattiti al di fuori di questo argomento, e la maggior parte di essi fatica a confrontare exceptionsvs restituire un codice di errore. Questo non è definitivamente l'argomento qui.

Cercando di definire un errore, sarei d'accordo con CppCoreGuidelines, da Bjarne Stroustrup & Herb Sutter

Un errore indica che la funzione non può raggiungere lo scopo pubblicizzato

RECLAMO: il exceptionmeccanismo è un linguaggio semantico per la gestione degli errori.

RECLAMO: Per me, non c'è "nessuna scusa" per una funzione per non raggiungere un compito: O abbiamo erroneamente definito le condizioni pre / post in modo che la funzione non possa garantire risultati, o qualche caso eccezionale specifico non è considerato abbastanza importante per passare il tempo nello sviluppo una soluzione. Considerando che, IMO, la differenza tra la gestione del codice normale e del codice di errore è (prima dell'implementazione) una linea molto soggettiva.

RECLAMO: L'uso delle eccezioni per indicare quando una condizione pre o post non è mantenuta è un altro scopo del exceptionmeccanismo, principalmente a scopo di debug. Non mi rivolgo a questo utilizzo di exceptionsqui.

In molti libri, tutorial e altre fonti, tendono a mostrare la gestione degli errori come una scienza abbastanza obiettiva, che è stata risolta exceptionse che hai solo bisogno di catchloro per avere un software robusto, in grado di recuperare da qualsiasi situazione. Ma i miei diversi anni come sviluppatore mi fanno vedere il problema da un approccio diverso:

  • I programmatori tendono a semplificare il loro compito generando eccezioni quando il caso specifico sembra troppo raro per essere implementato con attenzione. Casi tipici di questo sono: problemi di memoria insufficiente, problemi di disco pieno, problemi di file danneggiati, ecc. Ciò potrebbe essere sufficiente, ma non viene sempre deciso a livello di architettura.
  • I programmatori tendono a non leggere attentamente la documentazione relativa alle eccezioni nelle librerie e di solito non sono consapevoli di quale e quando una funzione viene generata. Inoltre, anche quando lo sanno, non li gestiscono davvero.
  • I programmatori tendono a non catturare le eccezioni abbastanza presto e, quando lo fanno, è soprattutto per accedere e lanciare ulteriormente. (fare riferimento al primo punto).

Ciò ha due conseguenze:

  1. Gli errori che si verificano frequentemente vengono rilevati all'inizio dello sviluppo e sottoposti a debug (il che è positivo).
  2. Le rare eccezioni non vengono gestite e causano l'arresto anomalo del sistema (con un bel messaggio di registro) nella home dell'utente. Alcune volte viene segnalato l'errore, o nemmeno.

Considerato ciò, l'IMO lo scopo principale di un meccanismo di errore dovrebbe essere:

  1. Rendi visibile nel codice in cui alcuni casi specifici non sono gestiti.
  2. Comunicare il runtime del problema al codice correlato (almeno il chiamante) quando si verifica questa situazione.
  3. Fornisce meccanismi di recupero

Il principale difetto della exceptionsemantica come meccanismo di gestione degli errori è l'IMO: è facile vedere dove si throwtrova a nel codice sorgente, ma non è assolutamente evidente sapere se una funzione specifica potrebbe essere lanciata guardando la dichiarazione. Questo porta tutto il problema che ho introdotto sopra.

Il linguaggio non impone e controlla il codice di errore in modo rigoroso come fa per altri aspetti del linguaggio (ad esempio, tipi di variabili forti)

Un tentativo di soluzione

Con l'intenzione di migliorare questo, ho sviluppato un sistema di gestione degli errori molto semplice, che cerca di mettere la gestione degli errori allo stesso livello di importanza del normale codice.

L'idea è:

  • Ogni funzione (rilevante) riceve un riferimento a un successoggetto molto leggero e, nel caso, può impostarlo su uno stato di errore. L'oggetto è molto leggero fino a quando non viene salvato un errore con il testo.
  • Una funzione è incoraggiata a saltare il suo compito se l'oggetto fornito contiene già un errore.
  • Un errore non deve mai essere ignorato.

Il design completo ovviamente considera attentamente ogni aspetto (circa 10 pagine), anche come applicarlo a OOP.

Esempio della Successclasse:

class Success
{
public:
    enum SuccessStatus
    {
        ok = 0,             // All is fine
        error = 1,          // Any error has been reached
        uninitialized = 2,  // Initialization is required
        finished = 3,       // This object already performed its task and is not useful anymore
        unimplemented = 4,  // This feature is not implemented already
    };

    Success(){}
    Success( const Success& v);
    virtual ~Success() = default;
    virtual Success& operator= (const Success& v);

    // Comparators
    virtual bool operator==( const Success& s)const { return (this->status==s.status && this->stateStr==s.stateStr);}
    virtual bool operator!=( const Success& s)const { return (this->status!=s.status || this->stateStr==s.stateStr);}

    // Retrieve if the status is not "ok"
    virtual bool operator!() const { return status!=ok;}

    // Retrieve if the status is "ok"
    operator bool() const { return status==ok;}

    // Set a new status
    virtual Success& set( SuccessStatus status, std::string msg="");
    virtual void reset();

    virtual std::string toString() const{ return stateStr;}
    virtual SuccessStatus getStatus() const { return status; }
    virtual operator SuccessStatus() const { return status; }

private:
    std::string stateStr;
    SuccessStatus status = Success::ok;
};

Uso:

double mySqrt( Success& s, double v)
{
    double result = 0.0;
    if (!s) ; // do nothing
    else if (v<0.0) s.set(Error, "Square root require non-negative input.");
    else result = std::sqrt(v);
    return result;
}

Success s;
mySqrt(s, 144.0);
otherStuff(s);
saveStuff(s);
if (s) /*All is good*/;
else cout << s << endl;

L'ho usato in molti dei miei (propri) codici e ho costretto il programmatore (io) a riflettere ulteriormente su possibili casi eccezionali e su come risolverli (bene). Tuttavia, ha una curva di apprendimento e non si integra bene con il codice che ora lo utilizza.

La domanda

Vorrei capire meglio le implicazioni dell'uso di un tale paradigma in un progetto:

  • La premessa al problema è corretta? o ho perso qualcosa di rilevante?
  • La soluzione è una buona idea architettonica? o il prezzo è troppo alto?

MODIFICARE:

Confronto tra metodi:

//Exceptions:

    // Incorrect
    File f = open("text.txt"); // Could throw but nothing tell it! Will crash
    save(f);

    // Correct
    File f;
    try
    {
        f = open("text.txt");
        save(f);
    }
    catch( ... )
    {
        // do something 
    }

//Error code (mixed):

    // Incorrect
    File f = open("text.txt"); //Nothing tell you it may fail! Will crash
    save(f);

    // Correct
    File f = open("text.txt");
    if (f) save(f);

//Error code (pure);

    // Incorrect
    File f;
    open(f, "text.txt"); //Easy to forget the return value! will crash
    save(f);

    //Correct
    File f;
    Error er = open(f, "text.txt");
    if (!er) save(f);

//Success mechanism:

    Success s;
    File f;
    open(s, "text.txt");
    save(s, f); //s cannot be avoided, will never crash.
    if (s) ... //optional. If you created s, you probably don't forget it.

25
Votato per "Questa domanda dimostra lo sforzo di ricerca, è utile e chiaro", non perché sono d'accordo: penso che alcuni dei pensieri siano sbagliati. (I dettagli possono seguire in una risposta.)
Martin Ba

2
Assolutamente, capisco e sono d'accordo! Lo scopo di questa domanda è di essere criticato. E il punteggio della domanda per indicare domande buone / cattive, non che l'OP ha ragione.
Adrian Maire,

2
Se ho capito bene, la tua lamentela principale riguardo alle eccezioni è che le persone possono ignorarlo (in c ++) invece di gestirle. Tuttavia, il costrutto Success ha lo stesso difetto di progettazione. Come le eccezioni, lo ignoreranno e basta. Ancora peggio: è più dettagliato, porta a rendimenti a cascata e non puoi nemmeno "catturarlo" a monte.
Dagnelies,

3
Perché non usare semplicemente qualcosa come le monadi? Rendono impliciti i tuoi errori ma non taceranno durante l'esecuzione. In realtà, la prima cosa che ho pensato guardando il tuo codice era "monads, nice". Dai un'occhiata a loro.
bash0r

2
Il motivo principale per cui mi piacciono le eccezioni è che ti consentono di rilevare tutti gli errori imprevisti da un determinato blocco di codice e gestirli in modo coerente. Sì, non c'è una buona ragione per cui il codice non dovrebbe svolgere il suo compito - "c'era un bug" è una cattiva ragione ma succede ancora , e quando succede vuoi registrare la causa e visualizzare un messaggio o riprovare. (Ho un po 'di codice che fa un'interazione complessa e riavviabile con un sistema remoto; se il sistema remoto dovesse andare in giù, voglio registrarlo e riprovare dall'inizio)
user253751

Risposte:


32

La gestione degli errori è forse la parte più difficile di un programma.

In generale, rendersi conto che esiste una condizione di errore è facile; tuttavia segnalarlo in un modo che non può essere eluso e gestirlo in modo appropriato (vedere i livelli di sicurezza delle eccezioni di Abrahams ) è davvero difficile.

In C, gli errori di segnalazione sono fatti da un codice di ritorno, che è isomorfo alla tua soluzione.

Il C ++ ha introdotto eccezioni a causa della mancanza di un simile approccio; vale a dire, funziona solo se i chiamanti si ricordano di verificare se si è verificato un errore o meno e si separano in modo orribile altrimenti. Ogni volta che ti ritrovi a dire "Va bene finché tutte le volte ..." hai un problema; gli umani non sono così meticolosi, anche quando gliene importa.

Il problema, tuttavia, è che le eccezioni hanno i loro problemi. Vale a dire, flusso di controllo invisibile / nascosto. Ciò era inteso: nascondere il caso di errore in modo che la logica del codice non venga offuscata dall'errore che gestisce la piastra di caldaia. Rende il "percorso felice" molto più chiaro (e veloce!), A costo di rendere quasi imperscrutabili i percorsi di errore.


Trovo interessante osservare come altre lingue affrontano il problema:

  • Java ha verificato le eccezioni (e quelle non selezionate),
  • Go utilizza codici di errore / panici,
  • Rust utilizza tipi di somma / panico).
  • Lingue FP in generale.

Il C ++ aveva una qualche forma di eccezioni verificate, potresti aver notato che è stato deprecato e semplificato verso una base noexcept(<bool>)invece: o una funzione viene dichiarata possibilmente lanciata, o è dichiarata mai. Le eccezioni verificate sono alquanto problematiche in quanto mancano di estensibilità, il che può causare mappature / nidificazione scomode. Gerarchie di eccezioni contorte (uno dei casi di utilizzo principale dell'ereditarietà virtuale sono le eccezioni ...).

Al contrario, Go e Rust adottano l'approccio che:

  • gli errori dovrebbero essere segnalati in banda,
  • l'eccezione dovrebbe essere usata per situazioni davvero eccezionali.

Quest'ultimo è piuttosto evidente in quanto (1) chiamano il loro panico eccezioni e (2) non c'è gerarchia dei tipi / clausola complicata qui. La lingua non offre servizi per ispezionare il contenuto di un "panico": nessuna gerarchia di tipi, nessun contenuto definito dall'utente, solo un "oops, le cose sono andate così male che non c'è possibilità di recupero".

Questo incoraggia efficacemente gli utenti a utilizzare la corretta gestione degli errori, lasciando comunque un modo semplice per salvare in situazioni eccezionali (come: "aspetta, non l'ho ancora implementato!").

Ovviamente, l'approccio Go sfortunatamente è molto simile al tuo in quanto puoi facilmente dimenticare di controllare l'errore ...

... l'approccio Rust tuttavia è principalmente incentrato su due tipi:

  • Option, che è simile a std::optional,
  • Result, che è una variante a due possibilità: Ok ed Err.

questo è molto più ordinato perché non c'è alcuna possibilità di utilizzare accidentalmente un risultato senza aver verificato il successo: se lo fai, il programma va nel panico.


I linguaggi FP formano la loro gestione degli errori in costrutti che possono essere divisi in tre livelli: - Functor - Applicativo / Alternativo - Monadi / Alternativo

Diamo un'occhiata alla Functortabella dei tipi di Haskell :

class Functor m where
  fmap :: (a -> b) -> m a -> m b

Prima di tutto, le macchine da scrivere sono in qualche modo simili ma non uguali alle interfacce. Le firme delle funzioni di Haskell sembrano un po 'spaventose al primo sguardo. Ma decifrali. La funzione fmapaccetta una funzione come primo parametro che è in qualche modo simile a std::function<a,b>. La prossima cosa è un m a. Puoi immaginare mcome qualcosa di simile std::vectore m acome qualcosa di simile std::vector<a>. Ma la differenza è che m aciò non significa che debba essere esplicitamente std:vector. Quindi potrebbe anche essere un std::option. Dicendo alla lingua che abbiamo un'istanza per la typeclass Functorper un tipo specifico come std::vectoro std::option, possiamo usare la funzione fmapper quel tipo. Lo stesso deve essere fatto per le typeclasses Applicative, AlternativeeMonadche consente di eseguire calcoli con stato, possibili non riusciti. La Alternativetypeclass implementa le astrazioni di recupero degli errori. Con ciò puoi dire qualcosa come a <|> bdire che è un termine ao un termine b. Se nessuno dei due calcoli riesce, rimane comunque un errore.

Diamo un'occhiata al Maybetipo di Haskell .

data Maybe a
  = Nothing
  | Just a

Ciò significa che dove ti aspetti un Maybe a, ottieni uno Nothingo Just a. Quando si guarda fmapdall'alto, un'implementazione potrebbe apparire come

fmap f m = case m of
  Nothing -> Nothing
  Just a -> Just (f a)

L' case ... ofespressione si chiama pattern matching e ricorda ciò che è noto nel mondo OOP visitor pattern. Immagina che la linea case m ofcome m.apply(...)e i punti siano l'istanza di una classe che implementa le funzioni di invio. Le righe sotto l' case ... ofespressione sono le rispettive funzioni di invio che portano i campi della classe direttamente nell'ambito per nome. Nel Nothingramo che creiamo Nothinge nel Just aramo nominiamo il nostro unico valore ae ne creiamo un altro Just ...con la funzione di trasformazione fapplicata a. Leggi come: new Just(f(a)).

Questo ora può gestire calcoli errati mentre si astraggono i controlli degli errori effettivi. Esistono implementazioni per le altre interfacce che rendono questo tipo di calcoli molto potente. In realtà, Maybeè l'ispirazione per Rust's Option-Type.


Vorrei incoraggiarvi a rielaborare la vostra Successclasse verso un Resultinvece. Alexandrescu in realtà ha proposto qualcosa di molto vicino, chiamato expected<T>, per il quale sono state fatte proposte standard .

Seguirò il nome e l'API di Rust semplicemente perché ... è documentato e funziona. Ovviamente, Rust ha un nobile ?operatore di suffisso che renderebbe il codice molto più dolce; in C ++, useremo l' espressione delle istruzioniTRY macro e GCC per emularla.

template <typename E>
struct Error {
    Error(E e): error(std::move(e)) {}

    E error;
};

template <typename E>
Error<E> error(E e) { return Error<E>(std::move(e)); }

template <typename T, typename E>
struct [[nodiscard]] Result {
    template <typename U>
    Result(U u): ok(true), data(std::move(u)), error() {}

    template <typename F>
    Result(Error<F> f): ok(false), data(), error(std::move(f.error)) {}

    template <typename U, typename F>
    Result(Result<U, F> other):
        ok(other.ok), data(std::move(other.data)),  error(std::move(other.error)) {}

    bool ok = false;
    T data;
    E error;
};

#define TRY(Expr_) \
    ({ auto result = (Expr_); \
       if (!result.ok) { return result; } \
       std::move(result.data); })

Nota: questo Resultè un segnaposto. Una corretta implementazione userebbe l'incapsulamento e a union. È comunque sufficiente chiarire il punto.

Il che mi permette di scrivere ( vederlo in azione ):

Result<double, std::string> sqrt(double x) {
    if (x < 0) {
        return error("sqrt does not accept negative numbers");
    }
    return x;
}

Result<double, std::string> double_sqrt(double x) {
    auto y = TRY(sqrt(x));
    return sqrt(y);
}

che trovo davvero pulito:

  • a differenza dell'uso dei codici di errore (o della tua Successclasse), la dimenticanza di verificare la presenza di errori comporterà un errore di runtime 1 anziché un comportamento casuale,
  • a differenza dell'uso delle eccezioni, nel sito della chiamata è evidente quali funzioni possono fallire, quindi non c'è sorpresa.
  • con lo standard C ++ - 2X, potremmo entrare conceptsnello standard. Ciò renderebbe questo tipo di programmazione molto più piacevole in quanto potremmo lasciare la scelta sul tipo di errore. Ad esempio, con un'implementazione di std::vectorcome risultato, potremmo calcolare tutte le possibili soluzioni contemporaneamente. Oppure potremmo scegliere di migliorare la gestione degli errori, come da lei proposto.

1 Con Resultun'implementazione correttamente incapsulata ;)


Nota: a differenza dell'eccezione, questo leggero Resultnon ha backtrace, il che rende la registrazione meno efficiente; potresti trovare utile registrare almeno il numero di file / riga in corrispondenza del quale viene generato il messaggio di errore e in genere scrivere un messaggio di errore avanzato. Questo può essere aggravato catturando il file / la linea ogni volta che TRYviene utilizzata la macro, essenzialmente creando manualmente il backtrace o usando codice e librerie specifici della piattaforma, come libbacktraceelencare i simboli nel callstack.


C'è però un grosso avvertimento: le librerie C ++ esistenti e persino stdsi basano su eccezioni. Sarà una dura battaglia utilizzare questo stile, poiché l'API di qualsiasi libreria di terze parti deve essere racchiusa in un adattatore ...


3
Quella macro sembra ... molto sbagliata. Presumo ({...})sia un'estensione di gcc, ma anche così, non dovrebbe essere if (!result.ok) return result;? La tua condizione appare al contrario e fai una copia non necessaria dell'errore.
Mooing Duck,

@MooingDuck La risposta spiega che ({...})è l' espressione delle dichiarazioni di gcc .
jamesdlin,


1
Ti consiglierei di usare std::variantper implementare Resultse stai usando C ++ 17. Inoltre, per ricevere un avviso se si ignora un errore, utilizzare[[nodiscard]]
Justin il

2
@Justin: se usare std::varianto meno è in qualche modo una questione di gusti, visti i compromessi relativi alla gestione delle eccezioni. [[nodiscard]]è davvero una vittoria pura.
Matthieu M.

46

RECLAMO: il meccanismo delle eccezioni è un linguaggio semantico per la gestione degli errori

le eccezioni sono un meccanismo di controllo del flusso. La motivazione di questo meccanismo di controllo del flusso, stava specificatamente separando la gestione degli errori dal codice di gestione degli errori, nel caso comune che la gestione degli errori sia molto ripetitiva e abbia scarsa rilevanza per la parte principale della logica.

RECLAMO: Per me, non c'è "nessuna scusa" per una funzione per non raggiungere un compito: O abbiamo erroneamente definito le condizioni pre / post in modo che la funzione non possa garantire risultati, o qualche caso eccezionale specifico non è considerato abbastanza importante per passare il tempo nello sviluppo una soluzione

Considera: provo a creare un file. Il dispositivo di archiviazione è pieno.

Ora, questo non è un errore nel definire i miei presupposti: non è possibile utilizzare "deve esserci spazio sufficiente" come prerequisito in generale, perché lo spazio condiviso è soggetto a condizioni di gara che lo rendono impossibile da soddisfare.

Quindi, il mio programma dovrebbe in qualche modo liberare spazio e quindi procedere con successo, altrimenti sono troppo pigro per "sviluppare una soluzione"? Questo sembra francamente senza senso. La "soluzione" per la gestione di storage condiviso è al di fuori della portata del mio programma , e permettendo il mio programma di fallire con grazia ed essere re-run, una volta che l'utente ha sia rilasciato un pò di spazio, o aggiunto un po 'più spazio di archiviazione, è bene .


Ciò che fa la tua classe di successo è interleave nella gestione degli errori in modo molto esplicito con la logica del tuo programma. Ogni singola funzione deve verificare, prima di essere eseguita, se si è già verificato un errore, il che significa che non dovrebbe fare nulla. Ogni funzione di libreria deve essere racchiusa in un'altra funzione, con un altro argomento (e si spera inoltro perfetto), che fa esattamente la stessa cosa.

Si noti inoltre che la mySqrtfunzione deve restituire un valore anche se non è riuscita (o se una funzione precedente non era riuscita). Quindi, stai restituendo un valore magico (come NaN), o iniettando un valore indeterminato nel tuo programma e sperando che nulla lo usi senza controllare lo stato di successo che hai superato durante l'esecuzione.

Per correttezza - e prestazioni - è molto meglio riportare il controllo fuori dall'ambito una volta che non si possono fare progressi. Le eccezioni e il controllo esplicito degli errori in stile C con ritorno anticipato consentono entrambe di ottenere questo risultato.


Per fare un confronto, un esempio della tua idea che funziona davvero è la monade Error in Haskell. Il vantaggio rispetto al tuo sistema è che scrivi normalmente la maggior parte della tua logica e poi la avvolgi nella monade che si occupa di interrompere la valutazione quando un passo fallisce. In questo modo, l'unico codice che tocca direttamente il sistema di gestione degli errori è il codice che potrebbe non riuscire (generare un errore) e il codice che deve affrontare l'errore (rilevare un'eccezione).

Non sono sicuro però che lo stile monade e la valutazione pigra si traducano bene in C ++.


1
Grazie alla tua risposta, aggiunge luce all'argomento. Immagino che l'utente non sarebbe d'accordo and allowing my program to fail gracefully, and be re-runquando avesse perso 2 ore di lavoro:
Adrian Maire,

14
La tua soluzione significa che in ogni posto in cui potresti creare un file, devi chiedere all'utente di risolvere la situazione e riprovare. Quindi ogni altra cosa che potrebbe andare storta, è anche necessario risolvere in qualche modo localmente. Con le eccezioni, prendi semplicemente std::exceptionil livello più alto dell'operazione logica, dici all'utente "X non riuscita a causa di ex.what ()" e offri di riprovare l'intera operazione quando e se sono pronti.
Inutile il

13
@AdrianMaire: il "consentire di fallire con grazia ed essere rieseguito" potrebbe anche essere implementato come showing the Save dialog again along with an error message and allowing the user to specify an alternative location to try. Questa è una gestione aggraziata di un problema che in genere non può essere fatto dal codice che rileva che il primo percorso di archiviazione è pieno.
Bart van Ingen Schenau,

3
La valutazione @Useless Lazy non ha nulla a che fare con l'uso della monade Error, come dimostrano linguaggi di valutazione rigorosi come Rust, OCaml e F # che tutti ne fanno un uso pesante.
settembre

1
@Useless IMO per il software di qualità, fa senso che “ogni luogo è possibile creare un file, è necessario richiedere all'utente di risolvere la situazione e riprovare”. I primi programmatori hanno spesso fatto notevoli progressi nel recupero degli errori, almeno il programma TeX di Knuth ne è pieno. E con il suo framework di "programmazione alfabetica" ha trovato un modo per mantenere la gestione degli errori in un'altra sezione, in modo che il codice rimanga leggibile e il recupero degli errori venga scritto con maggiore cura (perché quando si scrive la sezione di recupero degli errori, questo è il punto e il programmatore tende a fare un lavoro migliore).
ShreevatsaR,

15

Vorrei capire meglio le implicazioni dell'uso di un tale paradigma in un progetto:

  • La premessa al problema è corretta? o ho perso qualcosa di rilevante?
  • La soluzione è una buona idea architettonica? o il prezzo è troppo alto?

Il tuo approccio porta alcuni grossi problemi nel tuo codice sorgente:

  • si basa sul codice client ricordando sempre di controllare il valore di s. Questo è comune con l' uso dei codici di ritorno per l' approccio alla gestione degli errori e uno dei motivi per cui le eccezioni sono state introdotte nel linguaggio: con le eccezioni, se fallisci, non fallisci silenziosamente.

  • più codice scrivi con questo approccio, maggiore sarà anche il codice di errore sulla caldaia che dovrai aggiungere, per la gestione degli errori (il tuo codice non è più minimalista) e le tue attività di manutenzione aumentano.

Ma i miei diversi anni come sviluppatore mi fanno vedere il problema da un approccio diverso:

Le soluzioni a questi problemi dovrebbero essere affrontate a livello di lead tecnici o di team:

I programmatori tendono a semplificare il loro compito generando eccezioni quando il caso specifico sembra troppo raro per essere implementato con attenzione. Casi tipici di questo sono: problemi di memoria insufficiente, problemi di disco pieno, problemi di file danneggiati, ecc. Ciò potrebbe essere sufficiente, ma non viene sempre deciso a livello di architettura.

Se ti ritrovi a gestire ogni tipo di eccezione che può essere generata, sempre, il design non è buono; Quali errori vengono gestiti, dovrebbero essere decisi in base alle specifiche del progetto, non in base a come gli sviluppatori si sentono l'implementazione.

Indirizzo impostando test automatizzati, separando le specifiche dei test unitari e l'implementazione (due persone diverse fanno questo).

I programmatori tendono a non leggere attentamente la documentazione [...] Inoltre, anche quando lo sanno, non li gestiscono davvero.

Non ti occuperai di questo scrivendo più codice. Penso che la tua scommessa migliore sia la revisione meticolosa del codice applicato.

I programmatori tendono a non catturare le eccezioni abbastanza presto e, quando lo fanno, è soprattutto per accedere e lanciare ulteriormente. (fare riferimento al primo punto).

La corretta gestione degli errori è difficile, ma meno noiosa con le eccezioni che con i valori di ritorno (indipendentemente dal fatto che vengano effettivamente restituiti o passati come argomenti I / O).

La parte più delicata della gestione degli errori non è come si riceve l'errore, ma come assicurarsi che l'applicazione mantenga uno stato coerente in presenza di errori.

Per ovviare a questo, è necessario dedicare maggiore attenzione all'identificazione e al funzionamento in condizioni di errore (più test, più test unità / integrazione, ecc.).


12
Tutto il codice dopo un errore viene ignorato, se ricordi di controllare ogni volta che ricevi un'istanza come argomento . Questo è ciò che intendevo per "più codice scrivi con questo approccio, maggiore sarà anche il codice di errore che dovrai aggiungere". Dovrai indovinare il tuo codice con if sull'istanza di successo e ogni volta che lo dimentichi, è un bug. Il secondo problema causato dalla dimenticanza di verificare: il codice che viene eseguito fino a quando non si verifica di nuovo, non avrebbe dovuto essere eseguito affatto (continuando se si dimentica di controllare, corrompe i dati).
utnapistim,

11
No, la gestione di un'eccezione (o la restituzione di un codice di errore) non è un arresto anomalo, a meno che l'errore / l'eccezione non sia logicamente fatale o si scelga di non gestirla. Hai ancora la possibilità di gestire il caso di errore, senza dover controllare esplicitamente ad ogni passaggio se si è verificato un errore in precedenza
Inutile

11
@AdrianMaire In quasi tutte le applicazioni su cui lavoro, preferirei di gran lunga un arresto anomalo rispetto alla continuazione silenziosa. Lavoro su software business-critical in cui catturare un output negativo e continuare a utilizzarlo potrebbe comportare la perdita di molti soldi. Se la correttezza è cruciale e il crash accettabile, allora le eccezioni hanno un grande vantaggio qui.
Chris Hayes,

1
@AdrianMaire - Penso che sia molto più difficile dimenticare di gestire un'eccezione che il tuo metodo di dimenticare un'istruzione if ... Inoltre - il vantaggio principale delle eccezioni è quale livello li gestisce. È possibile che si desideri che un'eccezione di sistema si riempia ulteriormente per mostrare un messaggio di errore a livello di applicazione ma che gestisca situazioni di cui si è a conoscenza a un livello inferiore. Se stai usando librerie di terze parti o altri sviluppatori codice questa è davvero l'unica scelta ...
Milney

5
@Adrian Nessun errore, sembra che tu abbia letto male quello che ho scritto o che ho perso nella seconda metà. Il mio punto non è che l'eccezione verrà generata durante i test / sviluppo e che gli sviluppatori si renderanno conto che devono gestirli. Il punto è che la conseguenza di un'eccezione completamente non gestita nella produzione è preferibile alla conseguenza di un codice di errore non controllato. se perdi il codice di errore, ottieni e continui a utilizzare risultati errati. Se si perde l'eccezione, l'applicazione si arresta in modo anomalo e non continua a essere eseguita, non si ottengono risultati né risultati errati . (cont.)
Mr.Mindor,
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.