Gibt es berechtigte Gründe, Ausnahmeobjekte zurückzugeben, anstatt sie zu werfen?

45

Diese Frage gilt für alle OO-Programmiersprachen, die die Ausnahmebehandlung unterstützen. Ich verwende C # nur zu Illustrationszwecken.

Ausnahmen sollen normalerweise ausgelöst werden, wenn ein Problem auftritt, das der Code nicht sofort verarbeiten kann, und dann in einer catchKlausel an einer anderen Stelle (normalerweise einem äußeren Stapelrahmen) abgefangen werden .

F: Gibt es legitime Situationen, in denen Ausnahmen nicht ausgelöst und abgefangen, sondern einfach von einer Methode zurückgegeben und dann als Fehlerobjekte weitergegeben werden?

Diese Frage stellte sich für mich, weil die .NET 4- System.IObserver<T>.OnErrorMethode genau Folgendes vorschlägt: Ausnahmen, die als Fehlerobjekte weitergegeben werden.

Schauen wir uns ein anderes Szenario an, die Validierung. Angenommen, ich folge der üblichen Weisheit und unterscheide daher zwischen einem Fehlerobjekttyp IValidationErrorund einem separaten Ausnahmetyp ValidationException, mit dem unerwartete Fehler gemeldet werden:

partial interface IValidationError { }

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

(Der System.Component.DataAnnotationsNamespace verhält sich ähnlich.)

Diese Typen könnten wie folgt eingesetzt werden:

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(…)
}

Jetzt frage ich mich, ob ich das oben Gesagte nicht vereinfachen könnte:

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>
}

F: Was sind die Vor- und Nachteile dieser beiden unterschiedlichen Ansätze?

stakx
quelle
Wenn Sie nach zwei getrennten Antworten suchen, sollten Sie zwei getrennte Fragen stellen. Wenn Sie sie kombinieren, ist die Beantwortung umso schwieriger.
Bobson
2
@ Bobson: Ich sehe diese beiden Fragen als komplementär: Die erste Frage soll den allgemeinen Kontext des Themas allgemein definieren, während die zweite nach spezifischen Informationen fragt, die es den Lesern ermöglichen sollen, ihre eigenen Schlussfolgerungen zu ziehen. Bei einer Antwort müssen nicht beide Fragen einzeln und explizit beantwortet werden. Antworten "dazwischen" sind ebenso willkommen.
Stakx
5
Was ist der Grund, warum ValidationError von Exception abgeleitet wird, wenn Sie es nicht auslösen?
pdr
@pdr: Meinst du im Fall des Werfens AggregateExceptionstattdessen? Guter Punkt.
Stakx
3
Nicht wirklich. Ich meine, wenn du es werfen willst, benutze eine Ausnahme. Wenn Sie es zurückgeben möchten, verwenden Sie ein Fehlerobjekt. Wenn Sie Ausnahmen erwartet haben, die von Natur aus wirklich Fehler sind, fangen Sie sie ab und fügen Sie sie in Ihr Fehlerobjekt ein. Ob einer der beiden mehrere Fehler zulässt oder nicht, ist völlig irrelevant.
pdr

Antworten:

31

Gibt es legitime Situationen, in denen Ausnahmen nicht ausgelöst und abgefangen, sondern einfach von einer Methode zurückgegeben und dann als Fehlerobjekte weitergegeben werden?

Wenn es nie geworfen wird, ist es keine Ausnahme. Es ist ein Objekt derivedaus einer Exception-Klasse, obwohl es dem Verhalten nicht folgt. Zu diesem Zeitpunkt ist es eine reine Semantik, es eine Ausnahme zu nennen, aber ich sehe nichts Falsches daran, es nicht zu werfen. Aus meiner Sicht ist es genau dasselbe, eine Ausnahme nicht auszulösen, wie eine innere Funktion, die sie abfängt und ihre Ausbreitung verhindert.

Ist es für eine Funktion gültig, Ausnahmeobjekte zurückzugeben?

Absolut. Hier ist eine kurze Liste von Beispielen, wo dies angebracht sein kann:

  • Eine Ausnahmefabrik.
  • Ein Kontextobjekt, das meldet, ob ein früherer Fehler aufgetreten ist, und als betriebsbereite Ausnahme bezeichnet wird.
  • Eine Funktion, die eine zuvor festgestellte Ausnahme beibehält.
  • Eine Drittanbieter-API, die eine Ausnahme eines inneren Typs erstellt.

Ist es nicht schlimm zu werfen?

Eine Ausnahme auszulösen ist wie folgt: "Geh ins Gefängnis. Geh nicht vorbei!" im Brettspiel Monopoly. Es weist einen Compiler an, den gesamten Quellcode bis zum Abfangen zu überspringen, ohne einen dieser Quellcodes auszuführen. Es hat nichts mit Fehlern, dem Melden oder Stoppen von Fehlern zu tun. Ich denke gerne daran, eine Ausnahme als "super return" -Anweisung für eine Funktion auszulösen. Die Ausführung des Codes wird an eine Stelle zurückgegeben, die viel höher ist als die des ursprünglichen Aufrufers.

Das Wichtigste hierbei ist, zu verstehen, dass der wahre Wert von Ausnahmen im try/catchMuster und nicht in der Instanziierung des Ausnahmeobjekts liegt. Das Ausnahmeobjekt ist nur eine Statusnachricht.

In Ihrer Frage scheinen Sie die Verwendung dieser beiden Dinge zu verwechseln: Ein Sprung zu einem Handler der Ausnahme und der Fehlerzustand, den die Ausnahme darstellt. Nur weil Sie einen Fehlerstatus angenommen und ihn in eine Ausnahme eingeschlossen haben, bedeutet dies nicht, dass Sie dem try/catchMuster oder seinen Vorteilen folgen .

Reactgular
quelle
4
"Wenn es nie geworfen wird, ist es keine Ausnahme. Es ist ein Objekt, das von einer ExceptionKlasse abgeleitet wurde, aber nicht dem Verhalten folgt." - Und genau das ist das Problem: Ist es legitim, ein Exceptionabgeleitetes Objekt in einer Situation zu verwenden, in der es weder vom Hersteller des Objekts noch von einer aufrufenden Methode geworfen wird? wo es nur als "Zustandsnachricht" dient, die herumgereicht wird, ohne den Kontrollfluss "Super Return"? Oder bin ich zu verletzen einen unausgesprochenen try/ catch(Exception)Vertrag so schlecht , dass ich dies nie tun, und verwenden Sie stattdessen einen separaten, nicht ExceptionTypen?
Stakx
10
@ stakx-Programmierer haben und werden weiterhin Dinge tun, die für andere Programmierer verwirrend, aber technisch nicht inkorrekt sind. Deshalb habe ich gesagt, es sei Semantik. Die rechtliche Antwort ist, dass NoSie keine Verträge brechen. Das popular opinionwäre, dass Sie das nicht tun sollten. Die Programmierung steckt voller Beispiele, in denen Ihr Quellcode nichts falsch macht, aber jeder es nicht mag. Der Quellcode sollte immer so geschrieben werden, dass klar ist, was die Absicht ist. Helfen Sie wirklich einem anderen Programmierer, indem Sie auf diese Weise eine Ausnahme verwenden? Wenn ja, dann mach das. Ansonsten wundere dich nicht, wenn es dir nicht gefällt.
Reactgular
@cgTag Ihre Verwendung von Inline-Code-Blöcken für Kursivschrift macht mein Gehirn weh ..
Frayt
51

Das Zurückgeben von Ausnahmen, anstatt sie auszulösen, kann semantisch sinnvoll sein, wenn Sie eine Hilfsmethode zum Analysieren der Situation haben und eine entsprechende Ausnahme zurückgeben, die dann vom Aufrufer ausgelöst wird (Sie können dies eine "Ausnahmefactory" nennen). Das Auslösen einer Ausnahme in dieser Fehleranalysefunktion würde bedeuten, dass während der Analyse selbst ein Fehler aufgetreten ist, während das Zurückgeben einer Ausnahme bedeutet, dass die Art des Fehlers erfolgreich analysiert wurde.

Ein möglicher Anwendungsfall könnte eine Funktion sein, die HTTP-Antwortcodes in Ausnahmen verwandelt :

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);
     }
}

Beachten Sie, dass das Auslösen einer Ausnahme bedeutet, dass die Methode falsch verwendet wurde oder einen internen Fehler aufwies, während das Zurückgeben einer Ausnahme bedeutet, dass der Fehlercode erfolgreich identifiziert wurde.

Philipp
quelle
Dies hilft dem Compiler auch dabei, den Codefluss zu verstehen: Wenn eine Methode niemals korrekt zurückgibt (weil sie die "gewünschte" Ausnahme auslöst) und der Compiler davon nichts weiß, sind manchmal überflüssige returnAnweisungen erforderlich , damit der Compiler aufhört, sich zu beschweren.
Joachim Sauer
@ JoachimSauer Ja. Ich wünschte, sie würden C # mehr als einmal den Rückgabetyp "never" hinzufügen - dies würde bedeuten, dass die Funktion durch Auslösen beendet werden muss. Das Einfügen eines Rückgabewerts ist ein Fehler. Manchmal haben Sie Code, dessen einziger Job darin besteht, eine Ausnahme auszulösen.
Loren Pechtel
2
@LorenPechtel Eine Funktion, die niemals zurückgibt, ist keine Funktion. Es ist ein Terminator. Der Aufruf einer solchen Funktion löscht den aufrufenden Stack. Es ist ähnlich wie beim Telefonieren System.Exit(). Der Code nach dem Funktionsaufruf wird nicht ausgeführt. Das klingt nicht nach einem guten Entwurfsmuster für eine Funktion.
Reactgular
5
@MathewFoscarini Betrachten Sie eine Klasse mit mehreren Methoden, die auf ähnliche Weise Fehler verursachen können. Sie möchten einige Statusinformationen als Teil der Ausnahme sichern, um anzugeben, woran es gekaut hat, als es boomte. Sie möchten eine Kopie des Prettying-Up-Codes wegen DRY. Das bedeutet, dass entweder eine Funktion aufgerufen wird, die das Aufbessern vornimmt, und das Ergebnis in Ihrem Wurf verwendet, oder eine Funktion, die den aufbessernden Text aufbaut und ihn dann auslöst. Letztere finde ich generell überlegen.
Loren Pechtel
1
@LorenPechtel ah, ja das habe ich auch gemacht. Okay, ich verstehe jetzt.
Reactgular
5

Wenn das Ausnahmeobjekt das erwartete Ergebnis der Operation ist, dann ja. Die Fälle, an die ich denken kann, beinhalten jedoch immer irgendwann einen Wurffang:

  • Variationen des Musters "Try" (wobei eine Methode eine zweite Methode zum Auslösen von Ausnahmen einschließt, die Ausnahme jedoch abfängt und stattdessen einen Booleschen Wert zurückgibt, der den Erfolg angibt). Anstatt einen Booleschen Wert zurückzugeben, können Sie eine beliebige ausgelöste Ausnahme zurückgeben (null, wenn dies erfolgreich war). Auf diese Weise erhält der Endbenutzer mehr Informationen, während die Erfolgs- oder Fehleranzeige beibehalten wird.

  • Workflow-Fehlerbehandlung. Ähnlich wie bei der Kapselung von "try pattern" -Methoden ist möglicherweise ein Workflow-Schritt in einem Befehlskettenmuster abstrahiert. Wenn ein Glied in der Kette eine Ausnahme auslöst, ist es häufig einfacher, diese Ausnahme im abstrahierenden Objekt abzufangen und als Ergebnis dieser Operation zurückzugeben, sodass die Workflow-Engine und der eigentliche Code, die als Workflow-Schritt ausgeführt werden, keinen umfangreichen Versuch erfordern -Fanglogik für sich.

KeithS
quelle
Ich denke nicht, dass einige bemerkenswerte Leute eine solche Variation des "Try" -Musters befürworten, aber der "empfohlene" Ansatz, einen Booleschen zurückzugeben, scheint ziemlich anämisch. Ich würde denken, dass es vielleicht besser ist, eine abstrakte OperationResultKlasse mit der boolEigenschaft "Successful", einer GetExceptionIfAnyMethode und einer AssertSuccessfulMethode zu haben (die die Ausnahme auslösen würde, GetExceptionIfAnywenn sie nicht null ist). Nachdem GetExceptionIfAnyein Verfahren sein , anstatt eine Eigenschaft würde die Verwendung von statischen unveränderlichen Fehlerobjekten für Fälle ermöglichen , in denen es nicht viel sinnvoll zu sagen war.
Supercat
5

Nehmen wir an, Sie haben eine Aufgabe in einem Thread-Pool ausgeführt. Wenn diese Aufgabe eine Ausnahme auslöst, handelt es sich um einen anderen Thread, sodass Sie ihn nicht abfangen. Thread wo es ausgeführt wurde einfach sterben.

Stellen Sie sich nun vor, dass etwas (entweder Code in dieser Task oder die Thread-Pool-Implementierung) diese Ausnahme abfängt, und speichern Sie sie zusammen mit der Task, und betrachten Sie die Task als abgeschlossen (nicht erfolgreich). Jetzt können Sie fragen, ob die Aufgabe beendet ist (und entweder fragen, ob sie geworfen wurde oder ob sie erneut in Ihren Thread geworfen werden kann (oder besser, neue Ausnahme mit Original als Ursache)).

Wenn Sie es manuell machen, können Sie feststellen, dass Sie eine neue Ausnahme erstellen, diese auslösen und abfangen und dann in einem anderen Thread speichern, indem Sie sie abrufen, abfangen und darauf reagieren. Es kann also sinnvoll sein, das Werfen und Fangen zu überspringen, es einfach zu speichern und die Aufgabe zu beenden und dann einfach zu fragen, ob es eine Ausnahme gibt, und darauf zu reagieren. Dies kann jedoch zu einem komplizierteren Code führen, wenn an derselben Stelle wirklich Ausnahmen ausgelöst werden.

PS: Dies wurde mit Erfahrung in Java geschrieben, wo Stack-Trace-Informationen erstellt werden, wenn eine Ausnahme erstellt wird (im Gegensatz zu C #, wo sie beim Auslösen erstellt werden). Ein in Java nicht ausgelöster Ausnahmeansatz ist daher langsamer als in C # (sofern er nicht vorab erstellt und wiederverwendet wird), verfügt jedoch über Stack-Trace-Informationen.

Im Allgemeinen würde ich es vermeiden, eine Ausnahme zu erstellen und diese niemals auszulösen (es sei denn, die Leistungsoptimierung nach der Profilerstellung weist auf einen Engpass hin). Zumindest in Java, wo das Erstellen von Ausnahmen teuer ist (Stack-Trace). In C # ist es möglich, aber IMO ist es überraschend und sollte daher vermieden werden.

user470365
quelle
Dies tritt häufig auch bei Fehlerlistener-Objekten auf. Eine Warteschlange führt die Aufgabe aus, fängt eine Ausnahme ab und leitet diese Ausnahme an den zuständigen Fehlerlistener weiter. Es ist normalerweise nicht sinnvoll, den Task-Thread abzubrechen, der in diesem Fall nicht viel über den Job weiß. Diese Idee ist auch in die Java-APIs integriert, in denen ein Thread Ausnahmen von einem anderen übergeben bekommt: docs.oracle.com/javase/1.5.0/docs/api/java/lang/…
Lance Nanek
1

Es hängt von Ihrem Design ab, normalerweise würde ich einem Anrufer keine Ausnahmen zurückgeben, sondern sie werfen und fangen und dabei belassen. Normalerweise wird Code so geschrieben, dass er früh und schnell fehlschlägt. Betrachten Sie beispielsweise den Fall, dass eine Datei geöffnet und verarbeitet wird (dies ist 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 diesem Fall würden wir einen Fehler machen und die Verarbeitung der Datei beenden, sobald ein fehlerhafter Datensatz festgestellt wird.

Angenommen, wir möchten die Datei weiter verarbeiten, auch wenn eine oder mehrere Zeilen fehlerhaft sind. Der Code könnte ungefähr so ​​aussehen:

    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;
    }

In diesem Fall geben wir also die Ausnahme an den Aufrufer zurück, obwohl ich wahrscheinlich keine Ausnahme zurückgeben würde, sondern eine Art Fehlerobjekt, da die Ausnahme abgefangen und bereits behandelt wurde. Dies ist kein Nachteil beim Zurückgeben einer Ausnahme, aber andere Aufrufer könnten die Ausnahme erneut auslösen, was möglicherweise nicht wünschenswert ist, da das Ausnahmeobjekt dieses Verhalten aufweist. Wenn Sie möchten, dass Anrufer die Möglichkeit haben, eine Ausnahme erneut auszulösen, geben Sie andernfalls die Informationen aus dem Ausnahmeobjekt heraus, erstellen Sie ein kleineres, leichtes Objekt und geben Sie dieses stattdessen zurück. Das schnelle Versagen ist in der Regel sauberer Code.

In Ihrem Überprüfungsbeispiel würde ich wahrscheinlich keine Ausnahmeklasse erben oder Ausnahmen auslösen, da Überprüfungsfehler häufig vorkommen können. Es wäre weniger aufwändig, ein Objekt zurückzugeben, als eine Ausnahme auszulösen, wenn 50% meiner Benutzer ein Formular beim ersten Versuch nicht korrekt ausfüllen.

Jon Raynor
quelle
1

F: Gibt es legitime Situationen, in denen Ausnahmen nicht ausgelöst und abgefangen, sondern einfach von einer Methode zurückgegeben und dann als Fehlerobjekte weitergegeben werden?

Ja. Ich bin zum Beispiel kürzlich auf eine Situation gestoßen, in der ich festgestellt habe, dass ausgelöste Ausnahmen in einem ASMX-Webdienst kein Element in der resultierenden SOAP-Nachricht enthalten, sodass ich sie generieren musste.

Illustrativer Code:

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
Tom Tom
quelle
1

In den meisten Sprachen (afaik) gibt es Ausnahmen mit zusätzlichen Extras. Meist wird eine Momentaufnahme des aktuellen Stack-Trace im Objekt gespeichert. Dies ist nützlich für das Debuggen, kann aber auch Auswirkungen auf den Arbeitsspeicher haben.

Wie @ThinkingMedia bereits sagte, verwenden Sie die Ausnahme wirklich als Fehlerobjekt.

In Ihrem Codebeispiel tun Sie dies anscheinend hauptsächlich zur Wiederverwendung von Code und um Kompositionen zu vermeiden. Ich persönlich halte das nicht für einen guten Grund.

Ein weiterer möglicher Grund wäre, die Sprache so auszutricksen, dass Sie ein Fehlerobjekt mit einem Stack-Trace erhalten. Dies gibt Ihrem Fehlerbehandlungscode zusätzliche kontextbezogene Informationen.

Andererseits können wir davon ausgehen, dass das Aufrechterhalten der Stapelverfolgung Speicherkosten verursacht. Wenn Sie beispielsweise anfangen, diese Objekte irgendwo zu sammeln, kann dies zu einem unangenehmen Gedächtnisverlust führen. Dies hängt natürlich stark davon ab, wie Ausnahmestapel-Traces in der jeweiligen Sprache / Engine implementiert sind.

Also, ist es eine gute Idee?

Bisher habe ich nicht gesehen, dass es verwendet oder empfohlen wird. Das bedeutet aber nicht viel.

Die Frage ist: Ist der Stack-Trace wirklich nützlich für Ihre Fehlerbehandlung? Oder allgemeiner: Welche Informationen möchten Sie dem Entwickler oder Administrator bereitstellen?

Das typische Wurf / Versuch / Fang-Muster macht es irgendwie notwendig, einen Stapel-Trace zu haben, da Sie sonst keine Ahnung haben, woher er kommt. Für ein zurückgegebenes Objekt stammt es immer aus der aufgerufenen Funktion. Der Stack-Trace enthält möglicherweise alle Informationen, die der Entwickler benötigt. Möglicherweise reicht jedoch etwas weniger Schweres und Spezifischeres aus.

donquixote
quelle
-4

Die Antwort ist ja. Weitere Informationen zu Ausnahmen finden Sie unter http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Exceptions. Öffnen Sie den Pfeil für den C ++ - Stilleitfaden von Google. Sie können dort die Argumente sehen, für die sie sich entschieden haben, aber sagen, dass sie sich wahrscheinlich entschieden hätten, wenn sie es noch einmal tun müssten.

Es ist jedoch anzumerken, dass die Sprache Go aus ähnlichen Gründen auch nicht idiomatisch Ausnahmen verwendet.

btilly
quelle
1
Entschuldigung, aber ich verstehe nicht, wie diese Richtlinien bei der Beantwortung der Frage helfen: Sie empfehlen einfach, die Ausnahmebehandlung vollständig zu vermeiden. Das ist nicht das, wonach ich frage; Meine Frage ist, ob es legitim ist, einen Ausnahmetyp zu verwenden, um eine Fehlerbedingung in einer Situation zu beschreiben, in der das resultierende Ausnahmeobjekt niemals ausgelöst wird. Daran scheint sich in diesen Leitlinien nichts zu ändern.
Stakx