Eccezioni, codici di errore e sindacati discriminati


80

Di recente ho iniziato un lavoro di programmazione in C #, ma ho un po 'di esperienza in Haskell.

Ma capisco che C # è un linguaggio orientato agli oggetti, non voglio forzare un piolo circolare in un buco quadrato.

Ho letto l'articolo Eccezione da Microsoft che afferma:

NON restituire codici di errore.

Ma essendo abituato a Haskell, ho usato il tipo di dati C # OneOf, restituendo il risultato come valore "giusto" o l'errore (il più delle volte una enumerazione) come valore "sinistro".

Questo è molto simile alla convenzione di EitherHaskell.

A me questo sembra più sicuro delle eccezioni. In C #, ignorare le eccezioni non produce un errore di compilazione e, se non vengono rilevate, saltano fuori e bloccano il programma. Questo forse è meglio che ignorare un codice di errore e produrre comportamenti indefiniti, ma l'arresto anomalo del software del client non è ancora una buona cosa, in particolare quando sta eseguendo molte altre importanti attività aziendali in background.

Con OneOf, si deve essere abbastanza espliciti su decomprimerlo e gestire il valore di ritorno e i codici di errore. E se uno non sa come gestirlo in quella fase nello stack di chiamate, deve essere inserito nel valore restituito della funzione corrente, quindi i chiamanti sanno che potrebbe verificarsi un errore.

Ma questo non sembra essere l'approccio che Microsoft suggerisce.

Utilizzare OneOfinvece delle eccezioni per gestire le eccezioni "ordinarie" (come File Not Found ecc.) È un approccio ragionevole o è una pratica terribile?


Vale la pena notare che ho sentito che le eccezioni come flusso di controllo sono considerate un serio antipattern , quindi se l '"eccezione" è qualcosa che normalmente gestiresti senza terminare il programma, non è quel "flusso di controllo" in un certo senso? Capisco che ci sia un po 'di area grigia qui.

Nota che non sto usando OneOfcose come "Memoria esaurita", le condizioni che non mi aspetto di recuperare genereranno comunque delle eccezioni. Ma ho problemi abbastanza ragionevoli, come l'input dell'utente che non analizza è essenzialmente un "flusso di controllo" e probabilmente non dovrebbe generare eccezioni.


Pensieri successivi:

Da questa discussione quello che sto portando via attualmente è il seguente:

  1. Se ti aspetti che il chiamante immediato catchgestisca l'eccezione per la maggior parte del tempo e continui il suo lavoro, magari attraverso un altro percorso, probabilmente dovrebbe far parte del tipo restituito. Optionalo OneOfpuò essere utile qui.
  2. Se si prevede che il chiamante immediato non prenda l'eccezione per la maggior parte del tempo, emettere un'eccezione per salvare la stupidità di passarla manualmente nello stack.
  3. Se non sei sicuro di cosa farà il chiamante immediato, forse fornisci entrambi, come Parsee TryParse.

13
Usa F # Result;-)
Astrinus il

8
Un po 'fuori tema, ma nota che l'approccio che stai osservando ha la vulnerabilità generale che non c'è modo di garantire che lo sviluppatore abbia gestito correttamente l'errore. Certo, il sistema di digitazione può fungere da promemoria, ma perdi il default di far saltare il programma per non riuscire a gestire un errore, il che rende più probabile che finisca in uno stato imprevisto. Tuttavia, se il promemoria valga la perdita di tale inadempienza è un dibattito aperto. Sono propenso a pensare che sia semplicemente meglio scrivere tutto il codice supponendo che un'eccezione lo risolverà ad un certo punto; quindi il promemoria ha meno valore.
jpmc26,

9
Articolo correlato: Eric Lippert sui quattro "secchi" delle eccezioni . L'esempio che fornisce su eccezioni esogene mostra che ci sono scenari in cui devi solo affrontare le eccezioni, ad es FileNotFoundException.
Søren D. Ptæus,

7
Potrei essere solo su questo, ma penso che lo schianto sia buono . Non ci sono prove migliori che ci siano problemi sul tuo software di un registro degli arresti anomali con la traccia corretta. Se hai una squadra in grado di correggere e distribuire una patch in 30 minuti o meno, lasciare che quei brutti amici si presentino potrebbe non essere una brutta cosa.
T. Sar - Ripristina Monica il

9
Come mai nessuna delle risposte chiarisce che la Eithermonade non è un codice di errore , e nemmeno lo è OneOf? Sono fondamentalmente diversi e, di conseguenza, la domanda sembra basarsi su un malinteso. (Anche se in una forma modificata è ancora una domanda valida.)
Konrad Rudolph il

Risposte:


131

ma il crash del software del tuo client non è ancora una buona cosa

Sicuramente è una buona cosa.

Volete qualsiasi cosa che lasci il sistema in uno stato indefinito per fermare il sistema perché un sistema indefinito può fare cose cattive come dati corrotti, formattare il disco rigido e inviare e-mail minacciose al presidente. Se non è possibile ripristinare e riportare il sistema in uno stato definito, la causa responsabile è l'arresto anomalo. È esattamente il motivo per cui costruiamo sistemi che si arrestano in modo anomalo piuttosto che separarsi silenziosamente. Ora sicuramente, tutti desideriamo un sistema stabile che non si blocchi mai, ma lo vogliamo davvero solo quando il sistema rimane in uno stato sicuro prevedibile definito.

Ho sentito che le eccezioni come flusso di controllo sono considerate un grave antipattern

È assolutamente vero ma spesso è frainteso. Quando hanno inventato il sistema delle eccezioni avevano paura di interrompere la programmazione strutturata. Programmazione strutturata è per questo che abbiamo for, while, until, break, e continuequando tutti abbiamo bisogno, a fare tutto questo, è goto.

Dijkstra ci ha insegnato che l'uso di goto in modo informale (ovvero saltando ovunque ti piace) rende la lettura del codice un incubo. Quando ci hanno dato il sistema delle eccezioni avevano paura di reinventare il goto. Quindi ci dissero di non "usarlo per il controllo del flusso" sperando di capire. Sfortunatamente, molti di noi non lo fecero.

Stranamente, spesso non abusiamo delle eccezioni per creare il codice spaghetti come facevamo con goto. Il consiglio stesso sembra aver causato più problemi.

Fondamentalmente le eccezioni riguardano il rifiuto di un'ipotesi. Quando chiedi di salvare un file, supponi che il file possa e verrà salvato. L'eccezione che si ottiene quando non può essere dovuta al fatto che il nome è illegale, che l'HD è pieno o che un topo è rosicchiato attraverso il cavo dati. Puoi gestire tutti questi errori in modo diverso, puoi gestirli tutti allo stesso modo o puoi lasciarli fermare il sistema. C'è un percorso felice nel tuo codice in cui le tue assunzioni devono essere vere. In un modo o nell'altro le eccezioni ti portano fuori da quel percorso felice. A rigor di termini, sì, è una sorta di "controllo del flusso", ma non è quello che ti stavano avvertendo. Stavano parlando di sciocchezze come questa :

inserisci qui la descrizione dell'immagine

"Le eccezioni dovrebbero essere eccezionali". Questa piccola tautologia è nata perché i progettisti di sistemi di eccezione hanno bisogno di tempo per creare tracce di stack. Rispetto al saltare in giro, questo è lento. Si consuma tempo CPU. Ma se stai per registrare e arrestare il sistema o almeno interrompere l'elaborazione che richiede molto tempo prima di iniziare quello successivo, hai del tempo per uccidere. Se le persone iniziano a usare le eccezioni "per il controllo del flusso", quei presupposti sul tempo vanno tutti fuori dalla finestra. Quindi "Exceptions dovrebbe essere eccezionale" ci è stato dato davvero come considerazione delle prestazioni.

Molto più importante di così non ci sta confondendo. Quanto tempo hai impiegato per individuare il ciclo infinito nel codice sopra?

NON restituire codici di errore.

... è un buon consiglio quando ci si trova in una base di codice che in genere non utilizza codici di errore. Perché? Perché nessuno ricorderà di salvare il valore restituito e controllare i codici di errore. È ancora una bella convenzione quando sei in C.

OneOf

Stai usando un'altra convenzione. Va bene fintanto che stai organizzando la convention e non semplicemente combattendo un altro. È confuso avere due convenzioni di errore nella stessa base di codice. Se in qualche modo ti sei sbarazzato di tutto il codice che utilizza l'altra convenzione, vai avanti.

Mi piace la convention da solo. Una delle migliori spiegazioni che ho trovato qui * :

inserisci qui la descrizione dell'immagine

Ma per quanto mi piaccia, non lo mescolerò ancora con le altre convenzioni. Scegline uno e mantienilo. 1

1: Voglio dire, non farmi pensare a più di una convenzione contemporaneamente.


Pensieri successivi:

Da questa discussione quello che sto portando via attualmente è il seguente:

  1. Se ti aspetti che il chiamante immediato rilevi e gestisca l'eccezione per la maggior parte del tempo e continui il suo lavoro, magari attraverso un altro percorso, probabilmente dovrebbe far parte del tipo restituito. Opzionale o OneOf può essere utile qui.
  2. Se si prevede che il chiamante immediato non prenda l'eccezione per la maggior parte del tempo, emettere un'eccezione per salvare la stupidità di passarla manualmente nello stack.
  3. Se non sei sicuro di ciò che farà il chiamante immediato, potresti fornire entrambi, come Parse e TryParse.

Non è davvero così semplice. Una delle cose fondamentali che devi capire è cos'è uno zero.

Quanti giorni sono rimasti a maggio? 0 (perché non è maggio. È già giugno).

Le eccezioni sono un modo per rifiutare un'ipotesi, ma non sono l'unico modo. Se usi le eccezioni per rifiutare l'assunto, lasci la strada felice. Ma se hai scelto dei valori per inviare il percorso felice che segnala che le cose non sono così semplici come è stato ipotizzato, puoi rimanere su quel percorso finché può gestire quei valori. A volte 0 è già usato per significare qualcosa, quindi devi trovare un altro valore per mappare la tua ipotesi rifiutando l'idea. Puoi riconoscere questa idea dal suo uso nella buona vecchia algebra . Le monadi possono aiutare in questo, ma non deve sempre essere una monade.

Ad esempio 2 :

IList<int> ParseAllTheInts(String s) { ... }

Riesci a pensare a qualche buona ragione per cui questo deve essere progettato in modo da gettare deliberatamente qualsiasi cosa? Indovina cosa ottieni quando nessun int può essere analizzato? Non ho nemmeno bisogno di dirtelo.

Questo è un segno di un buon nome. Ci dispiace ma TryParse non è la mia idea di un buon nome.

Spesso evitiamo di gettare un'eccezione nel non ottenere nulla quando la risposta potrebbe essere più di una cosa allo stesso tempo, ma per qualche motivo se la risposta è una cosa o niente siamo ossessionati dall'insistere sul fatto che ci dia una cosa o lanci:

IList<Point> Intersection(Line a, Line b) { ... }

Le linee parallele devono davvero causare un'eccezione qui? È davvero così male se questo elenco non conterrà mai più di un punto?

Forse semanticamente non puoi prenderlo. Se è così, è un peccato. Ma forse Monads, che non ha una dimensione arbitraria come te List, ti farà sentire meglio.

Maybe<Point> Intersection(Line a, Line b) { ... }

Le Monadi sono piccole raccolte per scopi speciali che devono essere utilizzate in modi specifici che evitano di doverle testare. Dovremmo trovare il modo di gestirli indipendentemente da ciò che contengono. In questo modo il percorso felice rimane semplice. Se ti spacchi e metti alla prova ogni Monade che tocchi, li stai usando in modo sbagliato.

Lo so, è strano. Ma è un nuovo strumento (beh, per noi). Dagli un po 'di tempo. I martelli hanno più senso quando smetti di usarli sulle viti.


Se mi concedi, vorrei rispondere a questo commento:

Come mai nessuna delle risposte chiarisce che la monade non è un codice di errore, né OneOf? Sono fondamentalmente diversi e, di conseguenza, la domanda sembra basarsi su un malinteso. (Anche se in una forma modificata è ancora una domanda valida.) - Konrad Rudolph 4 giu 18 alle 14:08

Questo è assolutamente vero. Le monadi sono molto più vicine alle raccolte di eccezioni, bandiere o codici di errore. Realizzano contenitori fini per queste cose se usati con saggezza.


9
Non sarebbe più appropriato se la pista rossa fosse sotto le scatole, quindi non le attraversasse?
Flater

4
Logicamente, hai ragione. Tuttavia, ogni riquadro in questo particolare diagramma rappresenta un'operazione di associazione monadica. bind include (forse crea!) la traccia rossa. Consiglio di guardare la presentazione completa. È interessante e Scott è un grande oratore.
Gusdor,

7
Penso alle eccezioni più come un'istruzione COMEFROM piuttosto che un GOTO, dal momento throwche in realtà non sa / dice dove andremo;)
Warbo

3
Non c'è niente di sbagliato nel mescolare le convenzioni se è possibile stabilire dei limiti, che è l'incapsulamento.
Robert Harvey,

4
L'intera prima sezione di questa risposta recita fortemente come se smettessi di leggere la domanda dopo la frase citata. La domanda riconosce che l'arresto anomalo del programma è superiore al passaggio a comportamenti indefiniti: si tratta di arresti anomali in fase di runtime non con esecuzione continua e indefinita, ma piuttosto con errori di compilazione che ti impediscono persino di creare il programma senza considerare il potenziale errore e occuparsene (che potrebbe finire per essere un crash se non c'è altro da fare, ma sarà un crash perché lo vuoi, non perché lo hai dimenticato).
KRyan,

35

C # non è Haskell e dovresti seguire il consenso degli esperti della comunità C #. Se invece provi a seguire le pratiche di Haskell in un progetto C #, alienerai tutti gli altri membri del tuo team e alla fine scoprirai probabilmente i motivi per cui la comunità C # fa le cose in modo diverso. Una grande ragione è che C # non supporta convenientemente i sindacati discriminati.

Ho sentito che le eccezioni come flusso di controllo sono considerate un serio antipattern,

Questa non è una verità universalmente accettata. La scelta in ogni lingua che supporta le eccezioni è o generare un'eccezione (che il chiamante è libero di non gestire) o restituire un valore composto (che il chiamante DEVE gestire).

La propagazione delle condizioni di errore verso l'alto attraverso lo stack di chiamate richiede una condizione a tutti i livelli, raddoppiando la complessità ciclomatica di tali metodi e conseguentemente raddoppiando il numero di casi di test unitari. Nelle tipiche applicazioni aziendali, molte eccezioni vanno oltre il recupero e possono essere lasciate propagare al massimo livello (ad es. Il punto di accesso al servizio di un'applicazione Web).


9
La propagazione delle monadi non richiede nulla.
Basilevs,

23
@Basilevs Richiede il supporto per la propagazione di monadi mantenendo il codice leggibile, che C # non ha. Riceverai ripetute istruzioni if-return o catene di lambda.
Sebastian Redl,

4
@SebastianRedl lambdas sembra OK in C #.
Basilevs,

@Basilevs Dal punto di vista della sintassi sì, ma sospetto che dal punto di vista delle prestazioni potrebbero essere meno allettanti.
Pharap,

5
Nel moderno C # puoi probabilmente usare parzialmente le funzioni locali per tornare indietro. In ogni caso, la maggior parte dei software aziendali è rallentata in modo molto più significativo dall'IO rispetto ad altri.
Turksarama,

26

Sei venuto a C # in un momento interessante. Fino a tempi relativamente recenti, il linguaggio è rimasto saldamente nello spazio di programmazione imperativo. Usare eccezioni per comunicare errori era sicuramente la norma. L'uso dei codici di ritorno ha sofferto della mancanza di supporto linguistico (ad es. Attorno a sindacati discriminati e corrispondenza dei modelli). Abbastanza ragionevolmente, la guida ufficiale di Microsoft era di evitare i codici di ritorno e di usare le eccezioni.

Ci sono sempre state delle eccezioni a questo, sotto forma di TryXXXmetodi, che avrebbero restituito un booleanrisultato positivo e fornito un risultato di secondo valore tramite un outparametro. Questi sono molto simili ai modelli "try" nella programmazione funzionale, salvo che il risultato provenga da un parametro out, anziché da un Maybe<T>valore di ritorno.

Ma le cose stanno cambiando. La programmazione funzionale sta diventando ancora più popolare e linguaggi come C # stanno rispondendo. C # 7.0 ha introdotto alcune caratteristiche di base per la corrispondenza dei pattern nella lingua; C # 8 introdurrà molto di più, tra cui un'espressione switch, schemi ricorsivi ecc. Insieme a questo, c'è stata una crescita di "librerie funzionali" per C #, come la mia libreria Succinc <T> , che fornisce supporto anche ai sindacati discriminati .

Queste due cose combinate significano che un codice come il seguente sta lentamente crescendo in popolarità.

static Maybe<int> TryParseInt(string source) =>
    int.TryParse(source, out var result) ? new Some<int>(result) : none; 

public int GetNumberFromUser()
{
    Console.WriteLine("Please enter a number");
    while (true)
    {
        var userInput = Console.ReadLine();
        if (TryParseInt(userInput) is int value)
        {
            return value;
        }
        Console.WriteLine("That's not a valid number. Please try again");
    }
}

Al momento è ancora abbastanza di nicchia, anche se negli ultimi due anni ho notato un netto cambiamento dall'ostilità generale tra gli sviluppatori C # a tale codice a un crescente interesse nell'uso di tali tecniche.

Non ci siamo ancora nel dire " NON restituire i codici di errore". è antiquato e ha bisogno di ritirarsi. Ma la marcia lungo la strada verso quell'obiettivo è ben avviata. Quindi fai la tua scelta: segui i vecchi modi di generare eccezioni con l'abbandono gay se è così che ti piace fare le cose; oppure inizia a esplorare un nuovo mondo in cui le convenzioni dai linguaggi funzionali stanno diventando sempre più popolari in C #.


22
Questo è il motivo per cui odio C # :) Continuano ad aggiungere modi per scuoiare un gatto, e ovviamente i vecchi modi non vanno mai via. Alla fine non ci sono convenzioni per nulla, un programmatore perfezionista può sedersi per ore a decidere quale metodo è "più bello" e un nuovo programmatore deve imparare tutto prima di essere in grado di leggere il codice degli altri.
Aleksandr Dubinsky il

24
@AleksandrDubinsky, Hai riscontrato un problema fondamentale con i linguaggi di programmazione in generale, non solo con C #. Vengono create nuove lingue, snelle, fresche e piene di idee moderne. E pochissime persone li usano. Nel corso del tempo, ottengono utenti e nuove funzionalità, collegate a vecchie funzionalità che non possono essere rimosse perché rompono il codice esistente. Il gonfiore cresce. Quindi la gente crea nuove lingue che sono magre, fresche e piene di idee moderne ...
David Arno,

7
@AleksandrDubinsky non lo odi quando aggiungono ora funzionalità degli anni '60.
ctrl-alt-delor

6
@AleksandrDubinsky Yeah. Poiché Java ha provato ad aggiungere qualcosa una volta (generici), hanno fallito, ora dobbiamo convivere con il disastro orribile che ne è derivato, quindi hanno imparato la lezione e hanno smesso di aggiungere nulla
:)

3
@Agent_L: sai che Java ha aggiunto java.util.Stream(un IEnumerable-allo stesso modo) e lambda adesso, giusto? :)
cHao,

5

Penso che sia perfettamente ragionevole usare un DU per restituire errori, ma poi lo direi mentre scrivevo OneOf :) Ma lo direi comunque, poiché è una pratica comune in F # etc (ad esempio il tipo di risultato, come menzionato da altri ).

Non penso agli errori che di solito restituisco come situazioni eccezionali, ma invece come normali, ma si spera che raramente si siano verificate situazioni che possono o non devono essere gestite, ma dovrebbero essere modellate.

Ecco l'esempio dalla pagina del progetto.

public OneOf<User, InvalidName, NameTaken> CreateUser(string username)
{
    if (!IsValid(username)) return new InvalidName();
    var user = _repo.FindByUsername(username);
    if(user != null) return new NameTaken();
    var user = new User(username);
    _repo.Save(user);
    return user;
}

Generare eccezioni per questi errori sembra eccessivo: hanno un sovraccarico prestazionale, a parte gli svantaggi di non essere espliciti nella firma del metodo o di fornire una corrispondenza esaustiva.

In che misura OneOf è idiomatico in c #, questa è un'altra domanda. La sintassi è valida e ragionevolmente intuitiva. I DU e la programmazione orientata alla ferrovia sono concetti ben noti.

Vorrei salvare Eccezioni per cose che indicano che qualcosa è rotto dove possibile.


Quello che stai dicendo è che si OneOftratta di arricchire il valore di ritorno, come restituire una Nullable. Sostituisce le eccezioni per le situazioni che non sono eccezionali per cominciare.
Aleksandr Dubinsky,

Sì, e fornisce un modo semplice per eseguire una corrispondenza esaustiva nel chiamante, il che non è possibile utilizzando una gerarchia dei risultati basata sull'ereditarietà.
mcintyre321,

4

OneOfequivale a circa le eccezioni verificate in Java. Ci sono alcune differenze:

  • OneOfnon funziona con metodi che producono metodi chiamati per i loro effetti collaterali (poiché possono essere chiamati in modo significativo e il risultato semplicemente ignorato). Ovviamente, dovremmo tutti provare a usare le funzioni pure, ma questo non è sempre possibile.

  • OneOfnon fornisce informazioni su dove si è verificato il problema. Questo è buono in quanto salva le prestazioni (le eccezioni generate in Java sono costose a causa del riempimento della traccia dello stack e in C # sarà la stessa) e ti costringe a fornire informazioni sufficienti nel membro dell'errore OneOfstesso. È anche un male perché queste informazioni potrebbero essere insufficienti e potresti avere difficoltà a trovare l'origine del problema.

OneOf e l'eccezione controllata ha una "caratteristica" importante in comune:

  • entrambi ti costringono a gestirli ovunque nello stack di chiamate

Questo previene la tua paura

In C #, ignorare le eccezioni non produce un errore di compilazione e, se non vengono rilevate, saltano fuori e bloccano il programma.

ma come già detto, questa paura è irrazionale. Fondamentalmente, di solito c'è un unico posto nel tuo programma, dove devi catturare tutto (è probabile che utilizzerai già un framework per farlo). Dato che tu e il tuo team di solito passate settimane o anni a lavorare al programma, non lo dimenticherete, vero?

Il grande vantaggio delle eccezioni ignorabili è che possono essere gestite ovunque tu voglia gestirle, piuttosto che ovunque nella traccia dello stack. Questo elimina tonnellate di boilerplate (basta guardare un po 'di codice Java che dichiara "lancia ..." o racchiude eccezioni) e rende anche il codice meno buggy: come qualsiasi metodo può lanciare, devi essere consapevole di esso. Fortunatamente, l'azione giusta di solito non fa nulla , vale a dire lasciandola fluttuare in un posto dove può essere gestita in modo ragionevole.


+1 ma perché OneOf non funziona con gli effetti collaterali? (Immagino sia perché passerà un controllo se non assegni il valore restituito, ma poiché non conosco OneOf, né molto C # non sono sicuro - un chiarimento migliorerebbe la tua risposta.)
dcorking

1
È possibile restituire informazioni sull'errore con OneOf facendo in modo che l'oggetto restituito contenga informazioni utili, ad es. OneOf<SomeSuccess, SomeErrorInfo>Dove SomeErrorInfofornisce i dettagli dell'errore.
mcintyre321,

@dcorking Edited. Certo, se hai ignorato il valore di ritorno, allora non sai nulla su quale problema è successo. Se lo fai con una funzione pura, va bene (hai solo perso tempo).
maaartinus,

2
@ mcintyre321 Certo, puoi aggiungere informazioni, ma devi farlo manualmente ed è soggetto a pigrizia e dimenticanza. Immagino, in casi più complicati, una traccia dello stack sia più utile.
maaartinus,

4

L'uso di OneOf invece delle eccezioni per la gestione delle eccezioni "ordinarie" (come File Not Found ecc.) È un approccio ragionevole o è una pratica terribile?

Vale la pena notare che ho sentito che le eccezioni come flusso di controllo sono considerate un serio antipattern, quindi se l '"eccezione" è qualcosa che normalmente gestiresti senza terminare il programma, non è quel "flusso di controllo" in un certo senso?

Gli errori hanno diverse dimensioni. Individuo tre dimensioni: possono essere prevenute, con quale frequenza si verificano e se possono essere recuperate. Nel frattempo, la segnalazione di errori ha principalmente una dimensione: decidere fino a che punto battere il chiamante sopra la testa per costringerli a gestire l'errore, o lasciare che l'errore "tranquillamente" comporti un'eccezione.

Gli errori non prevenibili, frequenti e recuperabili sono quelli che devono davvero essere gestiti nel sito di chiamata. Giustificano meglio costringendo il chiamante ad affrontarli. In Java, li faccio verificare le eccezioni e un effetto simile si ottiene rendendoli Try*metodi o restituendo un'unione discriminata. I sindacati discriminati sono particolarmente utili quando una funzione ha un valore di ritorno. Sono una versione molto più raffinata del ritorno null. La sintassi dei try/catchblocchi non è eccezionale (troppe parentesi), rendendo le alternative un aspetto migliore. Inoltre, le eccezioni sono leggermente lente perché registrano le tracce dello stack.

Gli errori prevenibili (che non sono stati prevenuti a causa dell'errore / negligenza del programmatore) e gli errori non recuperabili funzionano davvero bene come eccezioni ordinarie. Gli errori rari funzionano anche meglio come eccezioni ordinarie poiché un programmatore può spesso pesare non vale la pena anticipare (dipende dallo scopo del programma).

Un punto importante è che spesso è il sito di utilizzo a determinare in che modo le modalità di errore di un metodo si adattano a queste dimensioni, da tenere presente quando si considera quanto segue.

Nella mia esperienza, forzare il chiamante ad affrontare condizioni di errore va bene quando gli errori non sono prevenibili, frequenti e recuperabili, ma diventa molto fastidioso quando il chiamante è costretto a saltare attraverso i cerchi quando non ne ha bisogno o non lo desidera . Ciò può essere dovuto al fatto che il chiamante sa che l'errore non si verificherà o che non vogliono (ancora) rendere il codice resiliente. In Java, questo spiega l'angoscia contro l'uso frequente di eccezioni controllate e il ritorno di Facoltativo (che è simile a Forse ed è stato recentemente introdotto in mezzo a molte controversie). In caso di dubbi, genera eccezioni e lascia che il chiamante decida come gestirle.

Infine, tieni presente che la cosa più importante delle condizioni di errore, molto al di sopra di qualsiasi altra considerazione come il modo in cui vengono segnalate, è documentarle accuratamente .


2

Le altre risposte hanno discusso eccezioni e codici di errore in modo sufficientemente dettagliato, quindi vorrei aggiungere un'altra prospettiva specifica alla domanda:

OneOf non è un codice di errore, è più simile a una monade

Il modo in cui viene utilizzato, OneOf<Value, Error1, Error2>è un contenitore che rappresenta il risultato effettivo o uno stato di errore. È come un Optional, tranne che quando non è presente alcun valore, può fornire maggiori dettagli sul perché.

Il problema principale con i codici di errore è che si dimentica di controllarli. Ma qui, letteralmente non è possibile accedere al risultato senza verificare la presenza di errori.

L'unico problema è quando non ti interessa il risultato. L'esempio della documentazione di OneOf fornisce:

public OneOf<User, InvalidName, NameTaken> CreateUser(string username) { ... }

... e quindi creano risposte HTTP diverse per ogni risultato, il che è molto meglio dell'uso delle eccezioni. Ma se chiami il metodo in questo modo.

public void CreateUserButtonClick(String username) {
    UserManager.CreateUser(string username)
}

allora non hai un problema e le eccezioni sarebbe la scelta migliore. Confronto di Rail savevs save!.


1

Una cosa da considerare è che il codice in C # generalmente non è puro. In una base di codice piena di effetti collaterali e con un compilatore che non si preoccupa di ciò che fai con un valore di ritorno di alcune funzioni, il tuo approccio può causare notevoli problemi. Ad esempio, se si dispone di un metodo che elimina un file, si desidera assicurarsi che l'applicazione si accorga quando il metodo ha avuto esito negativo, anche se non vi è motivo di controllare alcun codice di errore "sul percorso felice". Questa è una differenza considerevole rispetto ai linguaggi funzionali che richiedono di ignorare esplicitamente i valori restituiti che non si utilizzano. In C #, i valori restituiti vengono sempre implicitamente ignorati (l'unica eccezione sono i getter di proprietà IIRC).

Il motivo per cui MSDN ti dice "di non restituire i codici di errore" è esattamente questo: nessuno ti obbliga nemmeno a leggere il codice di errore. Il compilatore non ti aiuta affatto. Tuttavia, se la tua funzione è priva di effetti collaterali, puoi tranquillamente usare qualcosa del genere Either- il punto è che anche se ignori il risultato dell'errore (se presente), puoi farlo solo se non usi il "risultato corretto" o. Ci sono alcuni modi in cui puoi farlo - ad esempio, potresti consentire di "leggere" il valore solo passando un delegato per gestire il successo e i casi di errore e puoi facilmente combinarlo con approcci come la programmazione orientata alle ferrovie ( funziona benissimo in C #).

int.TryParseè da qualche parte nel mezzo. È puro. Definisce quali sono i valori dei due risultati in ogni momento, quindi sai che se il valore di ritorno è false, il parametro di output verrà impostato su 0. Non ti impedisce ancora di utilizzare il parametro output senza controllare il valore restituito, ma almeno hai la garanzia di quale sia il risultato anche se la funzione fallisce.

Ma una cosa assolutamente cruciale qui è la coerenza . Il compilatore non ti salverà se qualcuno cambia quella funzione per avere effetti collaterali. O per lanciare eccezioni. Quindi, sebbene questo approccio vada perfettamente bene in C # (e io lo uso), devi assicurarti che tutti nel tuo team lo capiscano e lo usino . Molti degli invarianti richiesti per il funzionamento ottimale della programmazione funzionale non sono applicati dal compilatore C # e devi assicurarti che vengano seguiti da soli. Devi essere consapevole delle cose che si rompono se non segui le regole che Haskell applica per impostazione predefinita - e questo è qualcosa che tieni a mente per qualsiasi paradigma funzionale che introduci nel tuo codice.


0

La frase corretta sarebbe: Non usare codici di errore per le eccezioni! E non usare le eccezioni per le non eccezioni.

Se succede qualcosa da cui non è possibile recuperare il tuo codice (ovvero farlo funzionare), questa è un'eccezione. Non c'è nulla che il tuo codice immediato possa fare con un codice di errore se non passandolo verso l'alto per registrare o forse fornire il codice all'utente in qualche modo. Tuttavia, questo è esattamente ciò a cui servono le eccezioni: si occupano di tutti i passaggi e aiutano ad analizzare ciò che è realmente accaduto.

Se tuttavia accade qualcosa, come l'input non valido che puoi gestire, riformattandolo automaticamente o chiedendo all'utente di correggere i dati (e hai pensato a quel caso), quella non è davvero un'eccezione in primo luogo - come te aspettatelo. Puoi sentirti libero di gestire quei casi con codici di errore o qualsiasi altra architettura. Solo non eccezioni.

(Nota che non ho molta esperienza in C #, ma la mia risposta dovrebbe valere nel caso generale in base al livello concettuale di quali sono le eccezioni.)


7
Fondamentalmente non sono d'accordo con la premessa di questa risposta. Se i progettisti del linguaggio non intendessero utilizzare eccezioni per errori recuperabili, la catchparola chiave non esisterebbe. E poiché stiamo parlando in un contesto C #, vale la pena notare che la filosofia qui sposata non è seguita dal framework .NET; forse l'esempio più semplice immaginabile di "input non valido che puoi gestire" è l'utente che digita un non-numero in un campo in cui è previsto un numero ... e int.Parsegenera un'eccezione.
Mark Amery,

@MarkAmery Per int.Parse non è nulla da cui riesca a recuperare né qualsiasi cosa si aspetti. La clausola catch viene in genere utilizzata per evitare il fallimento completo da una vista esterna, non chiederà all'utente nuove credenziali del database o simili, eviterà l'arresto anomalo del programma. È un errore tecnico, non un flusso di controllo dell'applicazione.
Frank Hopkins,

3
Il problema se è meglio che una funzione generi un'eccezione quando si verifica una condizione dipende dal fatto che il chiamante immediato della funzione sia in grado di gestire tale condizione. Nei casi in cui è possibile, generare un'eccezione che il chiamante immediato deve catturare rappresenta un lavoro extra per l'autore del codice chiamante e un lavoro extra per la macchina che lo elabora. Se un chiamante non sarà pronto a gestire una condizione, tuttavia, le eccezioni alleviano la necessità di avere il codice esplicito per il chiamante.
supercat

@Darkwing "Puoi sentirti libero di gestire quei casi con codici di errore o qualsiasi altra architettura". Sì, sei libero di farlo. Tuttavia non si ottiene alcun supporto da .NET per farlo, poiché lo stesso CL CL usa eccezioni invece di codici di errore. Quindi non solo in molti casi sei costretto a catturare le eccezioni di .NET e a restituire il codice di errore corrispondente invece di lasciare che l'eccezione si diffonda, ma stai anche forzando gli altri nel tuo team a un altro concetto di gestione degli errori che devono usare parallelamente alle eccezioni, il concetto standard di gestione degli errori.
Aleksander,

-2

È un '"idea terribile".

È terribile perché, almeno in .net, non hai un elenco esaustivo di possibili eccezioni. Qualsiasi codice può eventualmente generare un'eccezione. Soprattutto se stai facendo OOP e potresti chiamare metodi sovrascritti su un sottotipo anziché sul tipo dichiarato

Quindi dovresti cambiare tutti i metodi e le funzioni OneOf<Exception, ReturnType>, quindi se vuoi gestire diversi tipi di eccezione dovresti esaminare il tipo If(exception is FileNotFoundException)ecc.

try catch è l'equivalente di OneOf<Exception, ReturnType>

Modificare ----

Sono consapevole che proponete di tornare, OneOf<ErrorEnum, ReturnType>ma ritengo che questa sia in qualche modo una distinzione senza differenze.

Stai combinando codici di ritorno, eccezioni previste e diramazioni per eccezione. Ma l'effetto complessivo è lo stesso.


2
Le eccezioni "normali" sono anche un'idea terribile. l'indizio è nel nome
Ewan,

4
Non sto proponendo di tornare Exceptions.
Clinton,

stai proponendo che l'alternativa a uno dei è il flusso di controllo tramite eccezioni
Ewan,
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.