Das Problem:
Seit langer Zeit exceptions
mache 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, exceptions
einen 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 exception
Mechanismus 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 exception
Mechanismus, hauptsächlich zu Debug-Zwecken. Ich beziehe mich nicht auf diese Verwendung von exceptions
hier.
In vielen Büchern, Tutorials und anderen Quellen wird die Fehlerbehandlung als eine recht objektive Wissenschaft dargestellt, die mit gelöst wird, exceptions
und man braucht catch
sie 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:
- Häufig auftretende Fehler werden frühzeitig in der Entwicklung erkannt und behoben (was gut ist).
- 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:
- In Code sichtbar machen, in dem ein bestimmter Fall nicht verwaltet wird.
- Kommunizieren Sie die Problemlaufzeit mit dem zugehörigen Code (mindestens dem Aufrufer), wenn diese Situation eintritt.
- Bietet Wiederherstellungsmechanismen
Der Hauptfehler der exception
Semantik als Fehlerbehandlungsmechanismus ist IMO: Es ist leicht zu erkennen, wo sich a throw
im 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
success
sehr 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 Success
Klasse:
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.