Esistono motivi legittimi per restituire oggetti di eccezione anziché lanciarli?

44

Questa domanda è intesa per essere applicata a qualsiasi linguaggio di programmazione OO che supporti la gestione delle eccezioni; Sto usando C # solo per scopi illustrativi.

Solitamente le eccezioni sono intese per essere sollevate quando sorge un problema che il codice non può immediatamente gestire e quindi essere catturato in una clausola catch in una posizione diversa (di solito una cornice stack esterna).

Q: ci sono delle situazioni legittime in cui le eccezioni non vengono lanciate e catturate, ma semplicemente restituite da un metodo e poi passate in giro come oggetti di errore?

Questa domanda mi è venuta in mente perché il metodo System.IObserver<T>.OnError di .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 di errore IValidationError e un tipo di eccezione separato ValidationException che viene utilizzato per segnalare errori imprevisti:

partial interface IValidationError { }

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

(Lo spazio dei nomi System.Component.DataAnnotations fa qualcosa di molto simile.)

Questi tipi potrebbero essere utilizzati 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>
}

Q: quali sono i vantaggi e gli svantaggi di questi due diversi approcci?

    
posta stakx 05.08.2013 - 14:56
fonte

8 risposte

31

Are there any legitimate situations where exceptions are not thrown and caught, but simply returned from a method and then passed around as error objects?

Se non viene mai lanciato, non è un'eccezione. È un oggetto derived di una classe Exception, sebbene non segua il comportamento. Chiamarla un'eccezione è puramente semantica a questo punto, ma non vedo nulla di sbagliato nel non lanciarlo. Dal mio punto di vista, non lanciare un'eccezione è esattamente la stessa cosa di una funzione interna che la cattura e che impedisce di propagarsi.

È valido per una funzione restituire oggetti eccezionali?

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

  • Un factory di eccezione.
  • Un oggetto contesto che segnala se c'era 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 lo sta buttando male?

Lanciare un'eccezione è un po 'come: "Vai in prigione, non passare Go!" 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 interruzioni di bug. Mi piace pensare di lanciare un'eccezione come un'istruzione "super return" per una funzione. Restituisce l'esecuzione del codice in un punto molto più alto rispetto al chiamante originale.

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

Nella tua domanda sembra che tu stia confondendo l'uso di queste due cose: un salto 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 modello try/catch o i suoi vantaggi.

    
risposta data 05.08.2013 - 17:06
fonte
51

Restituire eccezioni invece di lanciarle può avere senso semantico quando si ha un metodo di supporto per analizzare la situazione e restituire un'eccezione appropriata che viene poi generata dal chiamante (si potrebbe chiamare una "fabbrica di eccezioni"). Lanciare un'eccezione in questa funzione di analisi degli errori significherebbe che qualcosa è andato storto durante l'analisi stessa, mentre restituire un'eccezione significa che il tipo di errore è stato analizzato correttamente.

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 lanciare un'eccezione significa che il metodo è stato usato errato o ha avuto un errore interno, mentre restituire un'eccezione significa che il codice di errore è stato identificato con successo.

    
risposta data 05.08.2013 - 16:39
fonte
5

Concettualmente, se l'oggetto eccezione è il risultato previsto dell'operazione, allora sì. I casi a cui riesco a pensare, comunque, coinvolgono sempre la cattura a un certo punto:

  • Variazioni sul pattern "Prova" (in cui un metodo incapsula un secondo metodo di lancio di eccezioni, ma rileva l'eccezione e restituisce invece un valore booleano che indica il successo). È possibile, invece di restituire un booleano, restituire qualsiasi eccezione generata (null se riuscita), consentendo all'utente finale ulteriori informazioni mantenendo l'indicazione di successo o di errore senza catch.

  • Elaborazione degli errori del flusso di lavoro. Simile nella pratica all'incapsulamento del metodo "try pattern", si potrebbe avere un passo del flusso di lavoro astratto in un modello chain-of-command. Se un collegamento nella catena genera un'eccezione, è spesso più pulito rilevare tale 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 un passaggio del flusso di lavoro non richiedono una prova estesa -la logica di cattura propria.

risposta data 05.08.2013 - 16:59
fonte
5

Supponiamo di aver assegnato alcune attività da eseguire in alcuni pool di thread. Se questa attività genera un'eccezione, è un thread diverso in modo da non essere rilevato. Thread in cui è stato eseguito solo morire.

Ora considera che qualcosa (o codice in quell'attività o l'implementazione del pool di thread) cattura quell'eccezione memorizzandola insieme all'attività e considera l'attività terminata (senza successo). Ora puoi chiedere se l'attività è finita (e chiedere se ha lanciato o può essere lanciata di nuovo nella tua discussione (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 un altro thread che recupera il lancio, cattura e reagisce su di esso. Quindi può avere senso saltare la presa e prendere e solo archiviarla e finire l'attività e poi chiedere se c'è un'eccezione e reagire su di essa. Ma questo può portare a un codice più complicato se nello stesso posto ci possono essere eccezioni davvero lanciate.

PS: questo è scritto con esperienza con Java, dove vengono create informazioni di traccia di stack quando viene creata un'eccezione (a differenza di C # in cui viene creata durante il lancio). L'approccio delle eccezioni non generate in Java sarà più lento rispetto a C # (a meno che non sia pre-elaborato e riutilizzato), ma disporrà di informazioni sulla traccia di stack disponibili.

In generale, eviterei di creare eccezioni e di non lanciarlo mai (a meno che l'ottimalizzazione delle prestazioni dopo la creazione del profilo non lo indichi come collo di bottiglia). Almeno in java, dove la creazione di eccezioni è costosa (traccia dello stack). In C # è possibile, ma IMO è sorprendente e quindi dovrebbe essere evitato.

    
risposta data 05.08.2013 - 17:47
fonte
1

Dipende dal tuo progetto, in genere non restituirei eccezioni a un chiamante, piuttosto vorrei buttarlo e prenderlo e lasciarlo a quello. 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, eseguiremo l'errore e interromperò l'elaborazione del file non appena ha rilevato un record non valido.

Ma diciamo che il requisito è che vogliamo continuare ad elaborare il file anche se una o più righe hanno un errore. Il codice potrebbe iniziare ad assomigliare 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 errore poiché l'eccezione è stata rilevata e già gestita. Non è un problema restituire un'eccezione, ma altri chiamanti potrebbero rigettare l'eccezione che potrebbe non essere desiderabile dato che l'oggetto eccezione ha quel comportamento. Se vuoi che i chiamanti abbiano la possibilità di rilanciare, allora restituisci un'eccezione, altrimenti, estrai le informazioni dall'oggetto eccezione e costruisci un oggetto leggero più piccolo e restituiscilo. Il fail veloce è in genere un codice più pulito.

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

    
risposta data 05.08.2013 - 22:01
fonte
1

Q: Are there any legitimate situations where exceptions are not thrown and caught, but simply returned from a method and then passed around as error objects?

Sì. Ad esempio, di recente ho riscontrato una situazione situazione in cui ho scoperto che le eccezioni generate in un servizio Web ASMX donot 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
    
risposta data 06.08.2013 - 00:09
fonte
1

Nella maggior parte delle lingue (afaik), le eccezioni vengono fornite con 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 @ThinkingMedia, stai usando l'eccezione come oggetto di errore.

Nel tuo esempio di codice, sembra che tu lo faccia principalmente per riutilizzare il codice ed evitare la composizione. Personalmente non penso che questa sia una buona ragione per farlo.

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

D'altra parte, possiamo supporre che mantenere traccia dello stack intorno abbia un costo in memoria. Per esempio. se inizi a raccogliere questi oggetti da qualche parte, potresti vedere uno spiacevole impatto sulla memoria. Ovviamente ciò dipende in gran parte dal modo in cui le tracce dello stack di eccezioni sono 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 schema throw / try / catch rende necessario avere una traccia stack, perché altrimenti non si ha idea di 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 basterebbe qualcosa di meno pesante e più specifico.

    
risposta data 20.12.2016 - 07:17
fonte
-4

La risposta è sì. Vedi link e apri la freccia per la guida in stile C ++ di Google sulle eccezioni. Puoi vedere lì gli argomenti contro cui erano soliti decidere, ma dire che se dovessero farlo di nuovo, avrebbero probabilmente deciso per.

Tuttavia vale la pena notare che la lingua Go non utilizza in modo idiologico le eccezioni, per ragioni simili.

    
risposta data 05.08.2013 - 18:40
fonte

Leggi altre domande sui tag