Überlegungen zur Fehlerbehandlung

31

Das Problem:

Seit langer Zeit exceptionsmache ich mir Sorgen um den Mechanismus, weil ich der Meinung bin, dass er nicht wirklich löst, was er sollte.

BEANTRAGUNG: Es gibt lange Debatten über dieses Thema, und die meisten von ihnen haben Schwierigkeiten, exceptionseinen Fehlercode zu vergleichen oder zurückzugeben. Dies ist definitiv nicht das Thema hier.

Beim Versuch, einen Fehler zu definieren, stimme ich CppCoreGuidelines von Bjarne Stroustrup & Herb Sutter zu

Ein Fehler bedeutet, dass die Funktion ihren angekündigten Zweck nicht erreichen kann

BEANTRAGUNG: Der exceptionMechanismus ist eine Sprachsemantik für die Behandlung von Fehlern.

Für mich gibt es keine Entschuldigung dafür, dass eine Funktion eine Aufgabe nicht erfüllt: Entweder haben wir die Vor- / Nachbedingungen falsch definiert, sodass die Funktion keine Ergebnisse erzielen kann, oder ein bestimmter Ausnahmefall wird als nicht wichtig genug angesehen, um Zeit in die Entwicklung zu investieren eine Lösung. Wenn man bedenkt, IMO, ist der Unterschied zwischen normalem Code und Fehlercode-Behandlung (vor der Implementierung) eine sehr subjektive Zeile.

BEANTRAGUNG: Die Verwendung von Ausnahmen, um anzuzeigen, wann eine Vor- oder Nachbedingung nicht eingehalten wird, ist ein weiterer Zweck des exceptionMechanismus, hauptsächlich zu Debug-Zwecken. Ich beziehe mich nicht auf diese Verwendung von exceptionshier.

In vielen Büchern, Tutorials und anderen Quellen wird die Fehlerbehandlung als eine recht objektive Wissenschaft dargestellt, die mit gelöst wird, exceptionsund man braucht catchsie nur, um eine robuste Software zu haben, die in der Lage ist, sich von jeder Situation zu erholen. Aber meine mehrjährige Erfahrung als Entwickler hat mich dazu veranlasst, das Problem aus einem anderen Blickwinkel zu betrachten:

  • Programmierer tendieren dazu, ihre Aufgabe zu vereinfachen, indem sie Ausnahmen auslösen, wenn der spezifische Fall zu selten erscheint, um sorgfältig implementiert zu werden. Typische Fälle hierfür sind: Probleme mit nicht genügend Arbeitsspeicher, Probleme mit vollem Datenträger, Probleme mit beschädigten Dateien usw. Dies ist möglicherweise ausreichend, wird jedoch nicht immer auf architektonischer Ebene entschieden.
  • Programmierer neigen dazu, die Dokumentation über Ausnahmen in Bibliotheken nicht sorgfältig zu lesen und wissen normalerweise nicht, welche und wann eine Funktion ausgelöst wird. Außerdem verwalten sie sie nicht wirklich, selbst wenn sie es wissen.
  • Programmierer neigen dazu, Ausnahmen nicht früh genug abzufangen, und wenn sie dies tun, ist es meistens, sie zu protokollieren und weiter zu werfen. (siehe ersten Punkt).

Dies hat zwei Konsequenzen:

  1. Häufig auftretende Fehler werden frühzeitig in der Entwicklung erkannt und behoben (was gut ist).
  2. Seltene Ausnahmen werden nicht verwaltet und führen dazu, dass das System beim Benutzer zu Hause abstürzt (mit einer netten Protokollmeldung). Manchmal wird der Fehler gemeldet oder gar nicht.

In Anbetracht dessen sollte IMO der Hauptzweck eines Fehlermechanismus sein:

  1. In Code sichtbar machen, in dem ein bestimmter Fall nicht verwaltet wird.
  2. Kommunizieren Sie die Problemlaufzeit mit dem zugehörigen Code (mindestens dem Aufrufer), wenn diese Situation eintritt.
  3. Bietet Wiederherstellungsmechanismen

Der Hauptfehler der exceptionSemantik als Fehlerbehandlungsmechanismus ist IMO: Es ist leicht zu erkennen, wo sich a throwim Quellcode befindet, aber es ist absolut nicht ersichtlich, ob eine bestimmte Funktion durch einen Blick auf die Deklaration ausgelöst werden könnte. Dies bringt all das Problem mit sich, das ich oben vorgestellt habe.

Die Sprache erzwingt und überprüft den Fehlercode nicht so streng, wie es für andere Aspekte der Sprache (z. B. starke Variablentypen) erforderlich ist.

Ein Versuch zur Lösung

In der Absicht, dies zu verbessern, habe ich ein sehr einfaches Fehlerbehandlungssystem entwickelt, das versucht, die Fehlerbehandlung auf die gleiche Wichtigkeit wie den normalen Code zu bringen.

Die Idee ist:

  • Jede (relevante) Funktion erhält einen Verweis auf ein successsehr leichtes Objekt und kann es gegebenenfalls in einen Fehlerstatus versetzen. Das Objekt ist sehr leicht, bis ein Fehler mit Text gespeichert wird.
  • Eine Funktion wird aufgefordert, ihre Aufgabe zu überspringen, wenn das bereitgestellte Objekt bereits einen Fehler enthält.
  • Ein Fehler darf niemals außer Kraft gesetzt werden.

Das vollständige Design berücksichtigt jeden Aspekt (ca. 10 Seiten) sorgfältig und auch die Anwendung auf OOP.

Beispiel der SuccessKlasse:

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

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

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

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

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

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

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

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

Verwendung:

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

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

Ich habe das in vielen meiner (eigenen) Codes verwendet und es zwingt den Programmierer (mich), über mögliche Ausnahmefälle nachzudenken und wie man sie löst (gut). Es hat jedoch eine Lernkurve und lässt sich nicht gut in Code integrieren, der es jetzt verwendet.

Die Frage

Ich möchte die Auswirkungen der Verwendung eines solchen Paradigmas in einem Projekt besser verstehen:

  • Stimmt die Voraussetzung für das Problem? oder Habe ich etwas Relevantes verpasst?
  • Ist die Lösung eine gute architektonische Idee? oder ist der preis zu hoch?

BEARBEITEN:

Methodenvergleich:

//Exceptions:

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

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

//Error code (mixed):

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

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

//Error code (pure);

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

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

//Success mechanism:

    Success s;
    File f;
    open(s, "text.txt");
    save(s, f); //s cannot be avoided, will never crash.
    if (s) ... //optional. If you created s, you probably don't forget it.
Adrian Maire
quelle
25
Upvoted for "Diese Frage zeigt den Forschungsaufwand; sie ist nützlich und klar", nicht weil ich damit einverstanden bin: Ich denke, einige der Gedanken sind falsch. (Details können in einer Antwort folgen.)
Martin Ba
2
Absolut, das verstehe und stimme ich zu! Es ist das Ziel dieser Frage, kritisiert zu werden. Und die Punktzahl der Frage zeigt gute / schlechte Fragen an, nicht dass das OP richtig ist.
Adrian Maire
2
Wenn ich das richtig verstehe, ist Ihr Hauptproblem bei Ausnahmen, dass die Leute es ignorieren können (in c ++), anstatt sie zu behandeln. Ihr Erfolgskonstrukt weist jedoch denselben Konstruktionsfehler auf. Wie Ausnahmen werden sie es einfach ignorieren. Schlimmer noch: Es ist ausführlicher, führt zu überlappenden Erträgen und man kann es nicht einmal stromaufwärts "fangen".
Dagnelies
3
Warum nicht einfach so etwas wie Monaden benutzen? Sie machen Ihre Fehler implizit, aber sie werden während des Laufs nicht still sein. Eigentlich war das erste, was ich dachte, als ich mir Ihren Code ansah, "Monaden, nett". Schau sie dir an.
Bash0r
2
Der Hauptgrund, warum ich Ausnahmen mag, ist, dass Sie damit alle unerwarteten Fehler eines bestimmten Codeblocks abfangen und konsistent behandeln können. Ja, es gibt keinen guten Grund, warum der Code seine Aufgabe nicht ausführen sollte - "Es gab einen Fehler" ist ein schlechter Grund, aber es passiert immer noch , und wenn es passiert, möchten Sie die Ursache protokollieren und eine Meldung anzeigen oder es erneut versuchen. (Ich habe Code, der eine komplexe, neu startbare Interaktion mit einem Remote-System ausführt. Sollte das Remote-System ausfallen, möchte ich ihn protokollieren und von Anfang an wiederholen.)
user253751

Antworten:

32

Fehlerbehandlung ist vielleicht der schwierigste Teil eines Programms.

Im Allgemeinen ist es einfach zu erkennen, dass ein Fehlerzustand vorliegt. Es ist jedoch sehr schwierig, es auf eine Art und Weise zu signalisieren, die nicht umgangen werden kann, und es angemessen zu behandeln (siehe Abrahams Ausnahmesicherheitsstufe ).

In C werden Signalisierungsfehler durch einen Rückkehrcode verursacht, der für Ihre Lösung isomorph ist.

C ++ Ausnahmen eingeführt , weil der Kurz kommenden eines solchen Ansatzes; Es funktioniert nämlich nur, wenn Anrufer daran denken, zu prüfen, ob ein Fehler aufgetreten ist oder nicht und scheitert ansonsten entsetzlich. Immer wenn Sie feststellen, dass Sie sagen: "Es ist in Ordnung, solange Sie ...", haben Sie ein Problem. Menschen sind nicht so akribisch, auch wenn sie sich darum kümmern.

Das Problem ist jedoch, dass Ausnahmen ihre eigenen Probleme haben. Unsichtbarer / versteckter Kontrollfluss. Dies war beabsichtigt: den Fehlerfall zu verbergen, damit die Logik des Codes nicht durch das Fehlerbehandlungs-Boilerplate verschleiert wird. Dies macht den "glücklichen Pfad" viel klarer (und schneller!), Was dazu führt, dass die Fehlerpfade nahezu undurchschaubar werden.


Ich finde es interessant zu sehen, wie andere Sprachen das Problem angehen:

  • Java hat Ausnahmen geprüft (und nicht angekreuzte),
  • Go verwendet Fehlercodes / Panics,
  • Rust verwendet Summentypen (Panik).
  • FP-Sprachen im Allgemeinen.

Früher gab es in C ++ eine Form von überprüften Ausnahmen. Möglicherweise haben Sie bemerkt, dass sie veraltet und noexcept(<bool>)stattdessen in Richtung Basic vereinfacht wurden : Entweder wird eine Funktion als möglicherweise auszulösen deklariert, oder sie wird als niemals auszulösen deklariert. Überprüfte Ausnahmen sind insofern problematisch, als ihnen die Erweiterbarkeit fehlt, was zu umständlichen Zuordnungen / Verschachtelungen führen kann. Und verschachtelte Ausnahmehierarchien (einer der Hauptanwendungsfälle der virtuellen Vererbung sind Ausnahmen ...).

Im Gegensatz dazu gehen Go und Rust folgendermaßen vor:

  • Fehler sollten im Band signalisiert werden,
  • Ausnahme sollte für wirklich außergewöhnliche Situationen verwendet werden.

Letzteres ist ziemlich offensichtlich , dass (1) sie ihre Ausnahmen nennen panics und (2) gibt es keine Typhierarchie / kompliziert Klausel hier. Die Sprache bietet keine Möglichkeit, den Inhalt einer "Panik" zu überprüfen: Keine Typhierarchie, kein benutzerdefinierter Inhalt, nur ein "Ups, es ist so schief gegangen, dass keine Wiederherstellung möglich ist".

Dies ermutigt die Benutzer effektiv, eine ordnungsgemäße Fehlerbehandlung zu verwenden, und bietet dennoch eine einfache Möglichkeit, in Ausnahmesituationen zu helfen (z. B .: "Warten Sie, das habe ich noch nicht implementiert!").

Natürlich ähnelt der Go-Ansatz leider dem Ihren, da Sie leicht vergessen können, den Fehler zu überprüfen ...

... der Rust-Ansatz konzentriert sich jedoch hauptsächlich auf zwei Arten:

  • Option, das ist ähnlich wie std::optional,
  • ResultDies ist eine Variante mit zwei Möglichkeiten: Ok und Err.

Dies ist viel übersichtlicher, da es keine Möglichkeit gibt, ein Ergebnis versehentlich zu verwenden, ohne es auf Erfolg überprüft zu haben. Andernfalls kommt das Programm in Panik.


FP-Sprachen bilden ihre Fehlerbehandlung in Konstrukten, die in drei Ebenen unterteilt werden können: - Functor - Applicative / Alternative - Monads / Alternative

FunctorWerfen wir einen Blick auf Haskells Typenklasse:

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

Zuallererst sind Typklassen etwas ähnlich, aber nicht gleich Schnittstellen. Haskells Funktionssignaturen sehen auf den ersten Blick etwas beängstigend aus. Aber lasst sie uns entziffern. Die Funktion fmapnimmt eine Funktion als ersten Parameter an, die etwas ähnlich zu ist std::function<a,b>. Das nächste ist ein m a. Sie können sich mso etwas std::vectorund m aso etwas vorstellen std::vector<a>. Aber der Unterschied ist, das m aheißt nicht, dass es explizit sein muss std:vector. So könnte es auch sein std::option. Indem wir der Sprache mitteilen, dass wir eine Instanz für die Typenklasse Functorfür einen bestimmten Typ wie std::vectoroder haben std::option, können wir die Funktion fmapfür diesen Typ verwenden. Das Gleiche muss für den typeclasses getan werden Applicative, AlternativeundMonadDadurch können Sie zustandsbehaftete, möglicherweise fehlgeschlagene Berechnungen durchführen. Die AlternativeTypenklasse implementiert Fehlerbehebungsabstraktionen. Damit kann man sagen, a <|> bdass es sich entweder um einen Begriff aoder um einen Begriff handelt b. Wenn keine der beiden Berechnungen erfolgreich ist, liegt immer noch ein Fehler vor.

Werfen wir einen Blick auf Haskells MaybeTyp.

data Maybe a
  = Nothing
  | Just a

Dies bedeutet, dass Sie dort, wo Sie ein erwarten Maybe a, entweder Nothingoder erhalten Just a. Wenn mann die fmapvon oben könnte eine Umsetzung aussehen

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

Der case ... ofAusdruck heißt Pattern Matching und ähnelt dem, was in der OOP-Welt als bekannt ist visitor pattern. Stellen Sie sich vor, die Zeile case m ofals m.apply(...)und die Punkte sind die Instanziierung einer Klasse, die die Dispatch-Funktionen implementiert. Die Zeilen unter dem case ... ofAusdruck sind die jeweiligen Versandfunktionen, mit denen die Felder der Klasse namentlich direkt in den Gültigkeitsbereich gebracht werden. In der NothingVerzweigung, die wir erstellen, Nothingund in der Just aVerzweigung benennen wir unseren einzigen Wert aund erstellen einen anderen Just ...mit der Transformationsfunktion, die auf fangewendet wird a. Lesen Sie es als: new Just(f(a)).

Dies kann nun fehlerhafte Berechnungen handhaben, während die eigentlichen Fehlerprüfungen entfernt werden. Es gibt Implementierungen für die anderen Schnittstellen, die diese Art von Berechnungen sehr leistungsfähig machen. Eigentlich Maybeist das die Inspiration für Rusts Option-Typ.


Ich würde Sie dort ermutigen, stattdessen Ihre SuccessKlasse zu überarbeiten Result. Alexandrescu schlug tatsächlich etwas sehr Nahes vor, genannt expected<T>, für das Standardvorschläge gemacht wurden .

Ich werde mich einfach an die Rust-Benennung und -API halten, weil ... sie dokumentiert ist und funktioniert. Natürlich hat Rust einen raffinierten ?Suffix-Operator, der den Code viel süßer machen würde. In C ++ werden wir den TRYMakro- und den GCC- Anweisungsausdruck verwenden , um ihn zu emulieren.

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

    E error;
};

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

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

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

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

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

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

Hinweis: Dies Resultist ein Platzhalter. Eine ordnungsgemäße Implementierung würde Kapselung verwenden und a union. Es ist jedoch ausreichend, den Punkt zu vermitteln.

Was mir erlaubt zu schreiben ( in Aktion zu sehen ):

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

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

was ich sehr ordentlich finde:

  • Im Gegensatz zur Verwendung von Fehlercodes (oder Ihrer SuccessKlasse) führt das Nichtbeachten von Fehlern zu einem Laufzeitfehler 1 und nicht zu einem zufälligen Verhalten.
  • Im Gegensatz zur Verwendung von Ausnahmen ist an der Aufrufstelle ersichtlich, welche Funktionen ausfallen können, sodass es keine Überraschung gibt.
  • Mit dem C ++ - 2X-Standard können wir conceptsin den Standard einsteigen . Dies würde diese Art der Programmierung weitaus angenehmer machen, da wir die Wahl über die Art der Fehler lassen könnten. ZB mit einer Implementierung von std::vectorals Ergebnis könnten wir alle möglichen Lösungen auf einmal berechnen. Oder wir könnten uns dafür entscheiden, die Fehlerbehandlung zu verbessern, wie Sie vorgeschlagen haben.

1 Mit einer richtig gekapselten ResultImplementierung;)


Hinweis: Im Gegensatz zur Ausnahme verfügt dieses Lightweight Resultüber keine Backtraces, wodurch die Protokollierung weniger effizient ist. Möglicherweise ist es hilfreich, zumindest die Datei- / Zeilennummer zu protokollieren, mit der die Fehlermeldung generiert wird, und im Allgemeinen eine umfangreiche Fehlermeldung zu verfassen. Dies kann durch Erfassen der Datei / Zeile bei jeder Verwendung des TRYMakros, im Wesentlichen durch manuelles Erstellen der Rückverfolgung oder durch Verwendung von plattformspezifischem Code und Bibliotheken, libbacktraceum die Symbole im Aufrufstapel aufzulisten, ergänzt werden.


Es gibt jedoch eine große Einschränkung: Bestehende C ++ - Bibliotheken und sogar solche std, die auf Ausnahmen basieren. Die Verwendung dieses Stils ist ein harter Kampf, da die API einer Drittanbieter-Bibliothek in einen Adapter eingeschlossen werden muss ...

Matthieu M.
quelle
3
Das Makro sieht ... sehr falsch aus. Ich gehe davon aus, dass ({...})es sich um eine GCC-Erweiterung handelt, aber sollte das nicht auch so sein if (!result.ok) return result;? Ihr Zustand erscheint rückwärts und Sie machen eine unnötige Kopie des Fehlers.
Mooing Duck
@MooingDuck Die Antwort erklärt, dass dies ({...})der Ausdruck von gccs Anweisungen ist .
Jamesdlin
1
Ich würde empfehlen , mit std::variantdem implementieren , Resultwenn Sie mit C ++ 17. Um eine Warnung zu erhalten, wenn Sie einen Fehler ignorieren, verwenden Sie[[nodiscard]]
Justin
2
@Justin: In std::variantAnbetracht der Kompromisse bei der Ausnahmebehandlung ist es eine Geschmackssache, ob man etwas verwendet oder nicht. [[nodiscard]]ist in der Tat ein reiner Gewinn.
Matthieu M.
46

ANTRAG: Der Ausnahmemechanismus ist eine Sprachsemantik für die Behandlung von Fehlern

Ausnahmen sind ein Kontrollflussmechanismus. Die Motivation für diesen Kontrollflussmechanismus bestand darin , die Fehlerbehandlung speziell vom Code für die fehlerfreie Behandlung zu trennen , in dem allgemeinen Fall, dass die Fehlerbehandlung sich sehr wiederholt und für den Hauptteil der Logik von geringer Relevanz ist.

Für mich gibt es keine Entschuldigung dafür, dass eine Funktion eine Aufgabe nicht erfüllt: Entweder haben wir die Vor- / Nachbedingungen falsch definiert, sodass die Funktion keine Ergebnisse liefern kann, oder ein bestimmter Ausnahmefall wird als nicht wichtig genug angesehen, um Zeit in die Entwicklung zu investieren eine Lösung

Bedenke: Ich versuche eine Datei zu erstellen. Das Speichergerät ist voll.

Nun, dies ist kein Fehler bei der Definition meiner Voraussetzungen: Sie können "Es muss genügend Speicher vorhanden sein" im Allgemeinen nicht als Voraussetzung verwenden, da der gemeinsame Speicher Wettkampfbedingungen unterliegt, die es unmöglich machen, diese zu erfüllen.

Sollte mein Programm also irgendwie Speicherplatz freigeben und dann erfolgreich fortfahren, sonst bin ich einfach zu faul, um "eine Lösung zu entwickeln"? Dies scheint ehrlich gesagt unsinnig. Die "Lösung" für die Verwaltung des gemeinsam genutzten Speichers liegt außerhalb des Anwendungsbereichs meines Programms , und es ist in Ordnung , wenn mein Programm ordnungsgemäß fehlschlägt und erneut ausgeführt wird, sobald der Benutzer Speicherplatz freigegeben oder weiteren Speicher hinzugefügt hat .


Ihre Erfolgsklasse verschachtelt die Fehlerbehandlung sehr explizit mit Ihrer Programmlogik. Jede einzelne Funktion muss vor dem Ausführen prüfen, ob bereits ein Fehler aufgetreten ist, was bedeutet, dass sie nichts tun sollte. Jede Bibliotheksfunktion muss in eine andere Funktion mit einem weiteren Argument (und hoffentlich perfekter Weiterleitung) eingeschlossen werden, die genau dasselbe tut.

Beachten Sie auch, dass Ihre mySqrtFunktion einen Wert zurückgeben muss, auch wenn dieser fehlgeschlagen ist (oder eine frühere Funktion fehlgeschlagen ist). Sie geben also entweder einen magischen Wert (wie NaN) zurück oder Sie fügen einen unbestimmten Wert in Ihr Programm ein und hoffen, dass nichts davon Gebrauch macht, ohne den Erfolgsstatus zu überprüfen, den Sie durch Ihre Ausführung gezogen haben.

Aus Gründen der Korrektheit und Leistung ist es viel besser, die Kontrolle wieder außer Reichweite zu bringen, wenn Sie keine Fortschritte erzielen können. Dies wird durch Ausnahmen und explizite Fehlerprüfung im C-Stil mit vorzeitiger Rückkehr erreicht.


Zum Vergleich: Ein Beispiel für Ihre Idee, die wirklich funktioniert, ist die Fehlermonade in Haskell. Der Vorteil gegenüber Ihrem System besteht darin, dass Sie den Großteil Ihrer Logik normal schreiben und dann in die Monade einbinden, um die Auswertung anzuhalten, wenn ein Schritt fehlschlägt. Auf diese Weise ist der einzige Code, der das Fehlerbehandlungssystem direkt berührt, der Code, der möglicherweise fehlschlägt (einen Fehler auslösen), und der Code, der den Fehler behandeln muss (eine Ausnahme abfangen).

Ich bin mir nicht sicher, ob Monadenstil und verzögerte Evaluierung sich gut in C ++ übersetzen lassen.

Nutzlos
quelle
1
Dank Ihrer Antwort wird das Thema beleuchtet. Ich denke, der Benutzer würde anderer Meinung sein, and allowing my program to fail gracefully, and be re-runwenn er gerade 2 Stunden Arbeit verloren hat:
Adrian Maire
14
Ihre Lösung bedeutet, dass Sie den Benutzer an jedem Ort, an dem Sie möglicherweise eine Datei erstellen, auffordern müssen, die Situation zu beheben und es erneut zu versuchen. Dann alles andere, was schief gehen könnte, muss man auch irgendwie lokal beheben. Mit Ausnahmen fangen Sie einfach std::exceptionauf der höheren Ebene der logischen Operation an, teilen dem Benutzer mit, dass X aufgrund von ex.what () fehlgeschlagen ist , und bieten an, die gesamte Operation zu wiederholen, wenn sie bereit ist.
Nutzlos
13
@AdrianMaire: Das "Erlauben, ordnungsgemäß zu versagen und erneut ausgeführt zu werden" könnte auch als implementiert werden showing the Save dialog again along with an error message and allowing the user to specify an alternative location to try. Dies ist eine ordnungsgemäße Behandlung eines Problems, die normalerweise nicht mit dem Code durchgeführt werden kann, der feststellt, dass der erste Speicherort voll ist.
Bart van Ingen Schenau
3
@Useless Lazy-Evaluierung hat nichts mit der Verwendung der Error-Monade zu tun, wie strenge Evaluierungssprachen wie Rust, OCaml und F # belegen, die alle stark davon Gebrauch machen.
8bittree
1
@Useless IMO für Qualitätssoftware, es macht Sinn , dass „jeder Ort , den Sie eine Datei erstellen können, müssen Sie den Benutzer aufzufordern , die Situation und die Neuversuch zu beheben“. Frühe Programmierer waren oft sehr bemüht, Fehler zu beheben, zumindest das TeX-Programm von Knuth ist voll davon. Und mit seinem Framework für „gebildete Programmierung“ hat er eine Möglichkeit gefunden, die Fehlerbehandlung in einem anderen Abschnitt beizubehalten, sodass der Code lesbar bleibt und die Fehlerbehebung mit größerer Sorgfalt geschrieben wird (denn wenn Sie den Abschnitt zur Fehlerbehebung schreiben, das ist der Punkt, und der Programmierer neigt dazu, einen besseren Job zu machen).
ShreevatsaR
15

Ich möchte die Auswirkungen der Verwendung eines solchen Paradigmas in einem Projekt besser verstehen:

  • Stimmt die Voraussetzung für das Problem? oder Habe ich etwas Relevantes verpasst?
  • Ist die Lösung eine gute architektonische Idee? oder ist der preis zu hoch?

Ihr Ansatz bringt einige große Probleme in Ihren Quellcode:

  • Es basiert auf dem Client-Code, der sich immer daran erinnert, den Wert von zu überprüfen s. Dies ist üblich bei der Verwendung von Rückkehrcodes für die Fehlerbehandlung der Fall und einer der Gründe, warum Ausnahmen in die Sprache eingefügt wurden: Mit Ausnahmen schlagen Sie bei einem Fehlschlag nicht unbemerkt fehl.

  • Je mehr Code Sie mit diesem Ansatz schreiben, desto mehr Fehlercode müssen Sie ebenfalls hinzufügen, um Fehler zu behandeln (Ihr Code ist nicht mehr minimalistisch) und Ihren Wartungsaufwand zu erhöhen.

Aber meine mehrjährige Erfahrung als Entwickler hat mich dazu gebracht, das Problem aus einem anderen Blickwinkel zu betrachten:

Die Lösungen für diese Probleme sollten auf technischer Ebene oder Teamebene erarbeitet werden:

Programmierer tendieren dazu, ihre Aufgabe zu vereinfachen, indem sie Ausnahmen auslösen, wenn der spezifische Fall zu selten erscheint, um sorgfältig implementiert zu werden. Typische Fälle hierfür sind: Probleme mit nicht genügend Arbeitsspeicher, Probleme mit vollem Datenträger, Probleme mit beschädigten Dateien usw. Dies ist möglicherweise ausreichend, wird jedoch nicht immer auf architektonischer Ebene entschieden.

Wenn Sie feststellen, dass Sie ständig mit jeder Art von Ausnahme fertig werden, ist das Design nicht gut. Welche Fehler behandelt werden, sollte anhand der Projektspezifikationen entschieden werden, nicht anhand der Art und Weise, in der Entwickler sich in der Umsetzung fühlen.

Lösung durch Einrichtung automatisierter Tests, Trennung von Spezifikation der Komponententests und Durchführung (zwei verschiedene Personen tun dies).

Programmierer neigen dazu, die Dokumentation nicht sorgfältig zu lesen, [...] und selbst wenn sie es wissen, verwalten sie sie nicht wirklich.

Sie werden dies nicht beheben, indem Sie mehr Code schreiben. Ich denke, Ihre beste Wette sind akribisch angewandte Codeüberprüfungen.

Programmierer neigen dazu, Ausnahmen nicht früh genug abzufangen, und wenn sie dies tun, ist es meistens, sie zu protokollieren und weiter zu werfen. (siehe ersten Punkt).

Eine ordnungsgemäße Fehlerbehandlung ist schwierig, aber mit Ausnahmen weniger mühsam als mit Rückgabewerten (unabhängig davon, ob sie tatsächlich zurückgegeben oder als E / A-Argumente übergeben werden).

Der schwierigste Teil der Fehlerbehandlung ist nicht, wie Sie den Fehler erhalten, sondern wie Sie sicherstellen, dass Ihre Anwendung bei Fehlern einen konsistenten Status beibehält.

Um dies zu beheben, muss der Identifizierung und Ausführung unter Fehlerbedingungen mehr Aufmerksamkeit gewidmet werden (mehr Tests, mehr Unit- / Integrationstests usw.).

utnapistim
quelle
12
Der gesamte Code nach einem Fehler wird übersprungen, wenn Sie daran denken, jedes Mal zu überprüfen , wenn Sie eine Instanz als Argument erhalten . Dies ist, was ich damit gemeint habe: "Je mehr Code Sie mit diesem Ansatz schreiben, desto mehr Fehlercode müssen Sie ebenfalls hinzufügen." Sie müssen Ihren Code mit ifs auf der Erfolgsinstanz rätseln, und jedes Mal, wenn Sie es vergessen, ist es ein Fehler. Das zweite Problem, das durch das Vergessen der Überprüfung verursacht wurde: Der Code, der ausgeführt wird, bis Sie ihn erneut überprüfen, sollte überhaupt nicht ausgeführt worden sein (Fahren Sie fort, wenn Sie die Überprüfung vergessen, beschädigen Sie Ihre Daten).
utnapistim
11
Nein, das Behandeln einer Ausnahme (oder das Zurückgeben eines Fehlercodes) ist kein Absturz - es sei denn, der Fehler / die Ausnahme ist logisch schwerwiegend oder Sie möchten sie nicht behandeln. Sie haben weiterhin die Möglichkeit , den Fehlerfall zu behandeln, ohne bei jedem Schritt explizit prüfen zu müssen, ob zuvor ein Fehler aufgetreten ist
Nutzlos
11
@AdrianMaire In fast jeder Anwendung, an der ich arbeite, würde ich einen Absturz der stillen Fortsetzung vorziehen. Ich arbeite an geschäftskritischer Software, bei der eine schlechte Ausgabe und die Fortsetzung der Arbeit mit ihr zu einem erheblichen Geldverlust führen können. Wenn die Korrektheit von entscheidender Bedeutung ist und ein Absturz akzeptabel ist, haben Ausnahmen hier einen sehr großen Vorteil.
Chris Hayes
1
@AdrianMaire - Ich denke, es ist viel schwieriger zu vergessen, eine Ausnahme zu behandeln, als Ihre Methode, eine if-Anweisung zu vergessen ... Außerdem - der Hauptvorteil von Ausnahmen ist, welche Ebene sie behandelt. Möglicherweise möchten Sie eine Systemausnahme weiter in die Luft sprudeln lassen, um eine Fehlermeldung auf Anwendungsebene anzuzeigen, aber Situationen, die Sie kennen, auf einer niedrigeren Ebene behandeln. Wenn Sie Bibliotheken von Drittanbietern oder Code anderer Entwickler verwenden, ist dies wirklich die einzige Wahl ...
Milney
5
@Adrian Kein Fehler, du scheinst falsch verstanden zu haben, was ich in der zweiten Hälfte geschrieben oder verpasst habe. Mein Punkt ist nicht, dass die Ausnahme während des Testens / der Entwicklung ausgelöst wird und dass die Entwickler erkennen, dass sie damit umgehen müssen. Der Punkt ist, dass die Konsequenz einer völlig unbehandelten Ausnahme in der Produktion der Konsequenz eines ungeprüften Fehlercodes vorzuziehen ist. Wenn Sie den Fehlercode verpassen, erhalten Sie und verwenden Sie weiterhin falsche Ergebnisse. Wenn Sie die Ausnahme verpassen, die Anwendung abstürzt und nicht weiter ausgeführt wird, erhalten Sie keine Ergebnisse, keine falschen Ergebnisse . (Forts.)
Mr.Mindor