Ci sono motivi legittimi per restituire oggetti di eccezione invece di lanciarli?


45

Questa domanda è destinata a qualsiasi linguaggio di programmazione OO che supporti la gestione delle eccezioni; Sto usando C # solo a scopo illustrativo.

Le eccezioni sono generalmente intese per essere sollevate quando sorge un problema che il codice non può gestire immediatamente, e quindi per essere catturate in una catchclausola in una posizione diversa (di solito un frame stack esterno).

D: Esistono situazioni legittime in cui le eccezioni non vengono generate e catturate, ma semplicemente restituite da un metodo e quindi passate in giro come oggetti di errore?

Questa domanda mi è venuta in mente perché il System.IObserver<T>.OnErrormetodo .NET 4 suggerisce proprio questo: le eccezioni vengono passate come oggetti di errore.

Diamo un'occhiata a un altro scenario, la convalida. Diciamo che sto seguendo la saggezza convenzionale e che sto quindi distinguendo tra un tipo di oggetto errore IValidationErrore un tipo di eccezione separato ValidationExceptionche viene utilizzato per segnalare errori imprevisti:

partial interface IValidationError { }

abstract partial class ValidationException : System.Exception
{
    public abstract IValidationError[] ValidationErrors { get; }
}

(Lo System.Component.DataAnnotationsspazio dei nomi fa qualcosa di abbastanza simile.)

Questi tipi potrebbero essere impiegati come segue:

partial interface IFoo { }  // an immutable type

partial interface IFooBuilder  // mutable counterpart to prepare instances of above type
{
    bool IsValid(out IValidationError[] validationErrors);  // true if no validation error occurs
    IFoo Build();  // throws ValidationException if !IsValid(…)
}

Ora mi chiedo, non potrei semplificare quanto sopra a questo:

partial class ValidationError : System.Exception { }  // = IValidationError + ValidationException

partial interface IFoo { }  // (unchanged)

partial interface IFooBuilder
{
    bool IsValid(out ValidationError[] validationErrors);
    IFoo Build();  // may throw ValidationError or sth. like AggregateException<ValidationError>
}

D: Quali sono i vantaggi e gli svantaggi di questi due approcci diversi?


Se stai cercando due risposte separate, dovresti porre due domande separate. Combinarli rende la risposta molto più difficile.
Bobson,

2
@Bobson: vedo queste due domande come complementari: la prima domanda dovrebbe definire il contesto generale della questione in termini generali, mentre la seconda chiede informazioni specifiche che dovrebbero consentire ai lettori di trarre le proprie conclusioni. Una risposta non deve dare risposte separate ed esplicite a entrambe le domande; le risposte "intermedie" sono altrettanto benvenute.
stakx,

5
Non sono chiaro su qualcosa: qual è la ragione per derivare ValidationError da Exception, se non hai intenzione di lanciarlo?
pdr,

@pdr: Intendi invece in caso di lancio AggregateException? Buon punto.
stakx,

3
Non proprio. Voglio dire che se hai intenzione di lanciarlo, usa un'eccezione. Se hai intenzione di restituirlo, usa un oggetto errore. Se hai previsto eccezioni che sono realmente errori per natura, prendile e inseriscile nel tuo oggetto errore. Se uno dei due consente più errori è del tutto irrilevante.
pdr,

Risposte:


31

Esistono situazioni legittime in cui le eccezioni non vengono generate e catturate, ma semplicemente restituite da un metodo e quindi passate in giro come oggetti di errore?

Se non viene mai lanciato, non fa eccezione. È un oggetto deriveddi una classe Exception, sebbene non segua il comportamento. Chiamarla un'eccezione è puramente semantica a questo punto, ma non vedo nulla di male nel non lanciarla. Dal mio punto di vista, non generare un'eccezione è esattamente la stessa cosa di una funzione interna che la cattura e ne impedisce la propagazione.

È valido per una funzione restituire oggetti eccezione?

Assolutamente. Ecco un breve elenco di esempi in cui potrebbe essere appropriato:

  • Una fabbrica d'eccezione.
  • Un oggetto di contesto che segnala se si è verificato un errore precedente come eccezione pronta per l'uso.
  • Una funzione che mantiene un'eccezione precedentemente rilevata.
  • Un'API di terze parti che crea un'eccezione di un tipo interno.

Non è lanciarlo male?

Lanciare un'eccezione è un po 'come: "Vai in galera. Non passare, vai!" nel gioco da tavolo Monopoli. Indica a un compilatore di saltare su tutto il codice sorgente fino alla cattura senza eseguire alcuno di quel codice sorgente. Non ha nulla a che fare con errori, segnalazioni o arresto di bug. Mi piace pensare di lanciare un'eccezione come un'istruzione "super return" per una funzione. Restituisce l'esecuzione del codice da qualche parte molto più in alto rispetto al chiamante originale.

La cosa importante qui è capire che il vero valore delle eccezioni è nel try/catchmodello e non l'istanza dell'oggetto eccezione. L'oggetto eccezione è solo un messaggio di stato.

Nella tua domanda sembri confondere l'uso di queste due cose: saltare a un gestore dell'eccezione e lo stato di errore che rappresenta l'eccezione. Solo perché hai preso uno stato di errore e lo hai racchiuso in un'eccezione non significa che stai seguendo il try/catchmodello o i suoi vantaggi.


4
"Se non viene mai lanciato, non è un'eccezione. È un oggetto derivato da una Exceptionclasse ma non segue il comportamento." - E questo è esattamente il problema: è legittimo usare un Exceptionoggetto derivato da una situazione in cui non viene affatto lanciato, né dal produttore dell'oggetto, né da alcun metodo di chiamata; dove funge solo da "messaggio di stato" che viene passato, senza il flusso di controllo "super return"? O sto violando un contratto try/ non detto catch(Exception)così gravemente che non dovrei mai farlo, e invece usare un non- Exceptiontipo separato ?
stakx,

10
I programmatori di @stakx hanno e continueranno a fare cose che confondono gli altri programmatori, ma non tecnicamente errate. Ecco perché ho detto che era semantica. La risposta legale è Noche non stai infrangendo alcun contratto. Il popular opinionsarebbe che non si deve fare. La programmazione è piena di esempi in cui il codice sorgente non fa nulla di male ma a tutti non piace. Il codice sorgente deve essere sempre scritto in modo che sia chiaro quale sia l'intento. Stai davvero aiutando un altro programmatore usando un'eccezione in quel modo? Se sì, allora fallo. Altrimenti non stupirti se non ti piace.
Reactgular

@cgTag L'utilizzo dei blocchi di codice inline per il corsivo mi fa male al cervello ..
Frayt,

51

Restituire le eccezioni invece di lanciarle può avere un senso semantico quando si dispone di un metodo di supporto per l'analisi della situazione e la restituzione di un'eccezione appropriata che viene quindi lanciata dal chiamante (è possibile definirla una "fabbrica di eccezioni"). Lanciare un'eccezione in questa funzione dell'analizzatore di errori significherebbe che qualcosa è andato storto durante l'analisi stessa, mentre restituire un'eccezione significa che il tipo di errore è stato analizzato con successo.

Un possibile caso d'uso potrebbe essere una funzione che trasforma i codici di risposta HTTP in eccezioni:

Exception analyzeHttpError(int errorCode) {
    if (errorCode < 400) {
         throw new NotAnErrorException();
    }
    switch (errorCode) {
        case 403:
             return new ForbiddenException();
        case 404:
             return new NotFoundException();
        case 500:
             return new InternalServerErrorException();
        …
        default:
             throw new UnknownHttpErrorCodeException(errorCode);
     }
}

Si noti che il lancio di un'eccezione significa che il metodo è stato utilizzato in modo errato o ha avuto un errore interno, mentre la restituzione di un'eccezione indica che il codice di errore è stato identificato correttamente.


Questo aiuta anche il compilatore a comprendere il flusso di codice: se un metodo non restituisce mai correttamente (perché genera l'eccezione "desiderata") e il compilatore non lo sa, a volte potresti aver bisogno di returnistruzioni superflue per far smettere di lamentarti.
Joachim Sauer,

@JoachimSauer Yeah. Più di una volta vorrei che aggiungessero un tipo di restituzione "mai" a C # - indicherebbe che la funzione deve uscire lanciando, inserendo un ritorno è un errore. A volte hai un codice il cui unico lavoro sta suscitando un'eccezione.
Loren Pechtel,

2
@LorenPechtel Una funzione che non ritorna mai non è una funzione. È un terminatore. Chiamare una funzione del genere cancella lo stack chiamante. È simile alla chiamata System.Exit(). Il codice dopo la chiamata di funzione non viene eseguito. Non sembra un buon modello di progettazione per una funzione.
Reactgular

5
@MathewFoscarini Considera una classe che ha più metodi che possono sbagliare in modi simili. Si desidera scaricare alcune informazioni sullo stato come parte dell'eccezione per indicare che cosa stava masticando quando è cresciuto. Volete una copia del codice di prettying-up a causa di DRY. Ciò lascia una chiamata a una funzione che esegue il prelievo e usa il risultato nel tuo tiro o significa una funzione che costruisce il testo pronto e poi lo lancia. In genere trovo quest'ultimo superiore.
Loren Pechtel,

1
@LorenPechtel ah, sì, l'ho fatto anche io. Ok adesso capisco.
Reactgular

5

Concettualmente, se l'oggetto eccezione è il risultato previsto dell'operazione, allora sì. I casi a cui riesco a pensare, tuttavia, comportano sempre il lancio del fermo ad un certo punto:

  • Variazioni sul modello "Try" (in cui un metodo incapsula un secondo metodo di generazione di eccezioni, ma rileva l'eccezione e restituisce invece un valore booleano che indica l'esito positivo). È possibile, invece di restituire un valore booleano, restituire qualsiasi eccezione generata (null se ha esito positivo), consentendo all'utente finale ulteriori informazioni mantenendo l'indicazione di successo o fallimento senza cattura.

  • Elaborazione degli errori del flusso di lavoro. Simile in pratica all'incapsulamento del metodo "prova modello", è possibile che un passaggio del flusso di lavoro venga sottratto in un modello a catena di comando. Se un collegamento nella catena genera un'eccezione, è spesso più pulito catturare quell'eccezione nell'oggetto di astrazione, quindi restituirlo come risultato di detta operazione in modo che il motore del flusso di lavoro e il codice effettivo vengano eseguiti come passaggio del flusso di lavoro non richiedono un tentativo approfondito -catching della propria logica.


Non credo che alcune persone degne di nota sostengano tale variazione nel modello "Prova", ma l'approccio "raccomandato" di restituire un booleano sembra piuttosto anemico. Penserei che sarebbe meglio avere una OperationResultclasse astratta , con una boolproprietà "Successful", un GetExceptionIfAnymetodo e un AssertSuccessfulmetodo (che genererebbe l'eccezione GetExceptionIfAnyse non nullo). Avere GetExceptionIfAnyun metodo piuttosto che una proprietà consentirebbe l'uso di oggetti di errore immutabili statici nei casi in cui non c'era molto da dire.
supercat

5

Diciamo che hai richiesto alcune attività da eseguire in un pool di thread. Se questa attività genera un'eccezione, è un thread diverso, quindi non lo prendi. Il thread dove è stato eseguito muore.

Ora considera che qualcosa (o il codice in quell'attività o l'implementazione del pool di thread) rileva quell'eccezione archiviandola insieme all'attività e considera l'attività terminata (senza successo). Ora puoi chiedere se l'attività è terminata (e chiedere se è stata lanciata o può essere nuovamente lanciata nel thread (o meglio, nuova eccezione con l'originale come causa)).

Se lo fai manualmente, puoi notare che stai creando una nuova eccezione, lanciandola e catturandola, quindi memorizzandola, in diversi thread recuperando il lancio, la cattura e la reazione su di essa. Quindi può avere senso saltare il lancio e catturare e semplicemente archiviarlo e terminare l'attività e quindi solo chiedere se c'è un'eccezione e reagire su di esso. Ma questo può portare a un codice più complicato se nello stesso posto possono esserci davvero delle eccezioni.

PS: questo è scritto con esperienza con Java, dove vengono create le informazioni di stack stack quando viene creata l'eccezione (a differenza di C # dove viene creato durante il lancio). Quindi l'approccio delle eccezioni non generato in Java sarà più lento che in C # (a meno che non sia precreato e riutilizzato), ma con le informazioni di traccia dello stack disponibili.

In generale, starei lontano dalla creazione dell'eccezione e dal non gettarla mai (a meno che l'ottimizzazione delle prestazioni dopo la profilazione lo indichi come un collo di bottiglia). Almeno in Java, dove la creazione di eccezioni è costosa (stack stack). In C # è possibile, ma IMO è sorprendente e quindi dovrebbe essere evitato.


Ciò accade spesso anche con oggetti listener di errori. Una coda eseguirà l'attività, rileverà qualsiasi eccezione, quindi passerà tale eccezione al listener di errori responsabile. Di solito non ha senso uccidere il thread di attività che in questo caso non sa molto del lavoro. Questo tipo di idea è integrata anche nelle API Java in cui un thread può superare le eccezioni da un altro: docs.oracle.com/javase/1.5.0/docs/api/java/lang/…
Lance Nanek,

1

Dipende dal tuo design, in genere non restituirei eccezioni a un chiamante, piuttosto le lancerei, le prenderei e le lascerei. In genere il codice viene scritto per fallire presto e velocemente. Ad esempio, considera il caso di aprire un file ed elaborarlo (questo è C # PsuedoCode):

        private static void ProcessFileFailFast()
        {
            try
            {
                using (var file = new System.IO.StreamReader("c:\\test.txt"))
                {
                    string line;
                    while ((line = file.ReadLine()) != null)
                    {
                        ProcessLine(line);
                    }
                }
            }
            catch (Exception ex) 
            {
                LogException(ex);
            }
        }

        private static void ProcessLine(string line)
        {
            //TODO:  Process Line
        }

        private static void LogException(Exception ex)
        {
            //TODO:  Log Exception
        }

In questo caso, verremmo eliminati e interrompere l'elaborazione del file non appena si verificava un record errato.

Ma supponiamo che vogliamo continuare a elaborare il file anche se una o più righe presentano un errore. Il codice potrebbe iniziare a somigliare a questo:

    private static void ProcessFileFailAndContinue()
    {
        try
        {
            using (var file = new System.IO.StreamReader("c:\\test.txt"))
            {
                string line;
                while ((line = file.ReadLine()) != null)
                {
                    Exception ex = ProcessLineReturnException(line);
                    if (ex != null)
                    {
                        _Errors.Add(ex);
                    }
                }
            }

            //Do something with _Errors Here
        }
        //Catch errors specifically around opening the file
        catch (System.IO.FileNotFoundException fnfe) 
        { 
            LogException(fnfe);
        }    
    }

    private static Exception ProcessLineReturnException(string line)
    {
        try
        {
            //TODO:  Process Line
        }
        catch (Exception ex)
        {
            LogException(ex);
            return ex;
        }

        return null;
    }

Quindi in questo caso restituiamo l'eccezione al chiamante, anche se probabilmente non restituirei un'eccezione ma piuttosto una sorta di oggetto di errore poiché l'eccezione è stata rilevata e già trattata. Questo non è dannoso nel restituire un'eccezione, ma altri chiamanti potrebbero rigettare l'eccezione che potrebbe non essere desiderabile poiché l'oggetto eccezione ha quel comportamento. Se si desidera che i chiamanti abbiano la possibilità di rilanciare, restituire un'eccezione, altrimenti, estrarre le informazioni dall'oggetto eccezione e costruire un oggetto leggero più piccolo e restituirlo invece. Il fail fast è di solito un codice più pulito.

Per il tuo esempio di validazione, probabilmente non erediterei da una classe di eccezioni né genererei eccezioni perché gli errori di validazione potrebbero essere abbastanza comuni. Sarebbe meno dispendioso restituire un oggetto piuttosto che generare un'eccezione se il 50% dei miei utenti non riuscisse a compilare un modulo correttamente al primo tentativo.


1

D: Esistono situazioni legittime in cui le eccezioni non vengono generate e catturate, ma semplicemente restituite da un metodo e quindi passate in giro come oggetti di errore?

Sì. Ad esempio, di recente ho riscontrato una situazione in cui ho riscontrato che le eccezioni generate in un servizio Web ASMX non includono un elemento nel messaggio SOAP risultante, quindi ho dovuto generarlo.

Codice illustrativo:

Public Sub SomeWebMethod()
    Try
        ...
    Catch ex As Exception
        Dim soapex As SoapException = Me.GenerateSoapFault(ex)
        Throw soapex
    End Try
End Sub

Private Function GenerateSoapFault(ex As Exception) As SoapException
    Dim document As XmlDocument = New XmlDocument()
    Dim faultDetails As XmlNode = document.CreateNode(XmlNodeType.Element, SoapException.DetailElementName.Name, SoapException.DetailElementName.Namespace)
    faultDetails.InnerText = ex.Message
    Dim exceptionType As String = ex.GetType().ToString()
    Dim soapex As SoapException = New SoapException("SoapException", SoapException.ClientFaultCode, Context.Request.Url.ToString, faultDetails)
    Return soapex
End Function

1

Nella maggior parte delle lingue (afaik), le eccezioni hanno alcune chicche aggiunte. Principalmente, un'istantanea della traccia dello stack corrente viene archiviata nell'oggetto. Questo è utile per il debug, ma può anche avere implicazioni sulla memoria.

Come già detto da @ThinkingMedia, stai davvero utilizzando l'eccezione come oggetto di errore.

Nel tuo esempio di codice, sembra che lo fai principalmente per il riutilizzo del codice e per evitare la composizione. Personalmente non penso che questo sia un buon motivo per farlo.

Un altro possibile motivo potrebbe essere ingannare la lingua per darti un oggetto di errore con una traccia dello stack. Ciò fornisce ulteriori informazioni contestuali al codice di gestione degli errori.

D'altra parte, possiamo presumere che mantenere la traccia dello stack abbia un costo di memoria. Ad esempio, se inizi a raccogliere questi oggetti da qualche parte, potresti notare un impatto spiacevole sulla memoria. Ovviamente ciò dipende in gran parte dal modo in cui le tracce dello stack delle eccezioni vengono implementate nel rispettivo linguaggio / motore.

Quindi è una buona idea?

Finora non l'ho visto essere usato o raccomandato. Ma questo non significa molto.

La domanda è: la traccia dello stack è davvero utile per la gestione degli errori? O più in generale, quali informazioni vuoi fornire allo sviluppatore o all'amministratore?

Il tipico modello di lancio / prova / cattura rende in qualche modo necessario avere una traccia dello stack, perché altrimenti non hai idea da dove provenga. Per un oggetto restituito, viene sempre dalla funzione chiamata. La traccia dello stack potrebbe contenere tutte le informazioni di cui lo sviluppatore ha bisogno, ma forse sarebbe sufficiente qualcosa di meno pesante e più specifico.


-4

La risposta è si. Vedi http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Exceptions e apri la freccia per la guida di stile C ++ di Google sulle eccezioni. Potete vedere lì gli argomenti contro i quali erano soliti decidere, ma dire che se avessero dovuto rifarlo di nuovo, probabilmente avrebbero deciso.

Tuttavia, vale la pena notare che la lingua Go non utilizza idiomaticamente nemmeno le eccezioni, per motivi simili.


1
Siamo spiacenti, ma non capisco in che modo queste linee guida aiutano a rispondere alla domanda: raccomandano semplicemente di evitare completamente la gestione delle eccezioni. Questo non è ciò di cui sto chiedendo; la mia domanda è se è legittimo utilizzare un tipo di eccezione per descrivere una condizione di errore in una situazione in cui l'oggetto dell'eccezione risultante non verrà mai generato. Non sembra esserci nulla al riguardo in queste linee guida.
stakx,
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.