Welche und warum bevorzugen Sie Ausnahmen oder Rückkehrcodes?

87

Meine Frage ist, was die meisten Entwickler für Fehlerbehandlung, Ausnahmen oder Fehlerrückgabecodes bevorzugen. Bitte seien Sie sprachspezifisch (oder sprachfamilienspezifisch) und warum Sie einander vorziehen.

Ich frage das aus Neugier. Persönlich bevorzuge ich Fehlerrückgabecodes, da sie weniger explosiv sind und den Benutzercode nicht zwingen, die Ausnahmeleistungsstrafe zu zahlen, wenn sie dies nicht möchten.

Update: Danke für alle Antworten! Ich muss das sagen, obwohl ich die Unvorhersehbarkeit des Codeflusses mit Ausnahmen nicht mag. Die Antwort über den Rückkehrcode (und die Handles ihres älteren Bruders) fügt dem Code viel Rauschen hinzu.

Robert Gould
quelle
1
Ein Henne-oder-Ei-Problem aus der Welt der Software ... ewig umstritten. :)
Gishu
Tut mir leid, aber hoffentlich hilft eine Vielzahl von Meinungen den Menschen (ich selbst eingeschlossen) bei der Auswahl.
Robert Gould

Antworten:

104

Für einige Sprachen (z. B. C ++) sollte ein Ressourcenleck kein Grund sein

C ++ basiert auf RAII.

Wenn Sie Code haben, der fehlschlagen, zurückgeben oder werfen kann (dh den meisten normalen Code), sollten Sie Ihren Zeiger in einen intelligenten Zeiger einschließen (vorausgesetzt, Sie haben einen sehr guten Grund, Ihr Objekt nicht auf einem Stapel zu erstellen).

Rückkehrcodes sind ausführlicher

Sie sind ausführlich und entwickeln sich zu etwas wie:

if(doSomething())
{
   if(doSomethingElse())
   {
      if(doSomethingElseAgain())
      {
          // etc.
      }
      else
      {
         // react to failure of doSomethingElseAgain
      }
   }
   else
   {
      // react to failure of doSomethingElse
   }
}
else
{
   // react to failure of doSomething
}

Am Ende ist Ihr Code eine Sammlung identifizierter Anweisungen (ich habe diese Art von Code im Produktionscode gesehen).

Dieser Code könnte gut übersetzt werden in:

try
{
   doSomething() ;
   doSomethingElse() ;
   doSomethingElseAgain() ;
}
catch(const SomethingException & e)
{
   // react to failure of doSomething
}
catch(const SomethingElseException & e)
{
   // react to failure of doSomethingElse
}
catch(const SomethingElseAgainException & e)
{
   // react to failure of doSomethingElseAgain
}

Welche sauber separater Code und Fehlerverarbeitung, die kann sein , gute Sache.

Rückkehrcodes sind spröder

Wenn nicht eine obskure Warnung von einem Compiler vorliegt (siehe den Kommentar von "phjr"), können sie leicht ignoriert werden.

Nehmen wir bei den obigen Beispielen an, dass jemand vergisst, seinen möglichen Fehler zu behandeln (dies passiert ...). Der Fehler wird bei "Rückgabe" ignoriert und explodiert möglicherweise später (dh ein NULL-Zeiger). Das gleiche Problem wird mit Ausnahme nicht auftreten.

Der Fehler wird nicht ignoriert. Manchmal möchten Sie jedoch, dass es nicht explodiert ... Sie müssen also sorgfältig auswählen.

Rückkehrcodes müssen manchmal übersetzt werden

Nehmen wir an, wir haben folgende Funktionen:

  • doSomething, das ein int namens NOT_FOUND_ERROR zurückgeben kann
  • doSomethingElse, das einen Bool "false" zurückgeben kann (für fehlgeschlagen)
  • doSomethingElseAgain, das ein Fehlerobjekt zurückgeben kann (sowohl mit __LINE__, __FILE__ als auch mit der Hälfte der Stapelvariablen.
  • doTryToDoSomethingWithAllThisMess welche, na ja ... Verwenden Sie die oben genannten Funktionen und geben Sie einen Fehlercode vom Typ ... zurück.

Was ist die Art der Rückgabe von doTryToDoSomethingWithAllThisMess, wenn eine der aufgerufenen Funktionen fehlschlägt?

Rückkehrcodes sind keine universelle Lösung

Bediener können keinen Fehlercode zurückgeben. C ++ - Konstruktoren können das auch nicht.

Rückkehrcodes bedeuten, dass Sie Ausdrücke nicht verketten können

Die Folge des obigen Punktes. Was ist, wenn ich schreiben möchte:

CMyType o = add(a, multiply(b, c)) ;

Ich kann nicht, da der Rückgabewert bereits verwendet wird (und manchmal nicht geändert werden kann). Der Rückgabewert wird also zum ersten Parameter, der als Referenz gesendet wird ... oder nicht.

Ausnahmen werden eingegeben

Sie können für jede Art von Ausnahme verschiedene Klassen senden. Ressourcenausnahmen (dh nicht genügend Speicher) sollten leicht sein, aber alles andere kann so schwer wie nötig sein (ich mag die Java-Ausnahme, die mir den gesamten Stapel gibt).

Jeder Fang kann dann spezialisiert werden.

Verwenden Sie niemals catch (...), ohne erneut zu werfen

Normalerweise sollten Sie einen Fehler nicht verbergen. Wenn Sie den Fehler zumindest nicht erneut auslösen, protokollieren Sie ihn in einer Datei, öffnen Sie eine Nachrichtenbox, was auch immer ...

Ausnahme sind ... NUKE

Das Problem mit Ausnahme ist, dass eine Überbeanspruchung Code voller Versuche / Fänge erzeugt. Das Problem ist jedoch anderswo: Wer versucht, seinen Code mithilfe eines STL-Containers abzufangen? Diese Container können jedoch eine Ausnahme senden.

Lassen Sie in C ++ natürlich niemals eine Ausnahme einen Destruktor verlassen.

Ausnahme sind ... synchron

Stellen Sie sicher, dass Sie sie abfangen, bevor sie Ihren Thread auf die Knie zwingen oder sich in Ihrer Windows-Nachrichtenschleife verbreiten.

Die Lösung könnte darin bestehen, sie zu mischen?

Ich denke, die Lösung ist zu werfen, wenn etwas nicht passieren sollte. Und wenn etwas passieren kann, verwenden Sie einen Rückkehrcode oder einen Parameter, damit der Benutzer darauf reagieren kann.

Die einzige Frage ist also: "Was sollte nicht passieren?"

Dies hängt vom Vertrag Ihrer Funktion ab. Wenn die Funktion einen Zeiger akzeptiert, aber angibt, dass der Zeiger nicht NULL sein darf, ist es in Ordnung, eine Ausnahme auszulösen, wenn der Benutzer einen NULL-Zeiger sendet (die Frage ist in C ++, wann der Funktionsautor keine Referenzen verwendet hat von Zeigern, aber ...)

Eine andere Lösung wäre, den Fehler anzuzeigen

Manchmal ist Ihr Problem, dass Sie keine Fehler wollen. Die Verwendung von Ausnahmen oder Fehlerrückgabecodes ist cool, aber ... Sie möchten es wissen.

In meinem Job verwenden wir eine Art "Assert". Abhängig von den Werten einer Konfigurationsdatei wird unabhängig von den Debug- / Release-Kompilierungsoptionen Folgendes ausgeführt:

  • Protokollieren Sie den Fehler
  • Öffne eine Messagebox mit einem "Hey, du hast ein Problem"
  • Öffnen Sie eine Nachrichtenbox mit der Meldung "Hey, Sie haben ein Problem, möchten Sie debuggen?"

Auf diese Weise kann der Benutzer sowohl beim Entwickeln als auch beim Testen das Problem genau bestimmen, wenn es erkannt wird, und nicht danach (wenn sich ein Code um den Rückgabewert kümmert oder innerhalb eines Catch).

Es ist einfach, Legacy-Code hinzuzufügen. Beispielsweise:

void doSomething(CMyObject * p, int iRandomData)
{
   // etc.
}

führt eine Art Code ähnlich wie:

void doSomething(CMyObject * p, int iRandomData)
{
   if(iRandomData < 32)
   {
      MY_RAISE_ERROR("Hey, iRandomData " << iRandomData << " is lesser than 32. Aborting processing") ;
      return ;
   }

   if(p == NULL)
   {
      MY_RAISE_ERROR("Hey, p is NULL !\niRandomData is equal to " << iRandomData << ". Will throw.") ;
      throw std::some_exception() ;
   }

   if(! p.is Ok())
   {
      MY_RAISE_ERROR("Hey, p is NOT Ok!\np is equal to " << p->toString() << ". Will try to continue anyway") ;
   }

   // etc.
}

(Ich habe ähnliche Makros, die nur beim Debuggen aktiv sind).

Beachten Sie, dass die Konfigurationsdatei in der Produktion nicht vorhanden ist, sodass der Client das Ergebnis dieses Makros nie sieht. Es ist jedoch einfach, es bei Bedarf zu aktivieren.

Fazit

Wenn Sie mit Rückkehrcodes codieren, bereiten Sie sich auf einen Fehler vor und hoffen, dass Ihre Testfestung sicher genug ist.

Wenn Sie mit Ausnahme codieren, wissen Sie, dass Ihr Code fehlschlagen kann, und setzen normalerweise den Gegenfeuerfang an der ausgewählten strategischen Position in Ihrem Code. Aber normalerweise geht es in Ihrem Code mehr um "was es tun muss" als um "was ich fürchte".

Wenn Sie jedoch überhaupt codieren, müssen Sie das beste Tool verwenden, das Ihnen zur Verfügung steht. Manchmal lautet es "Verstecken Sie niemals einen Fehler und zeigen Sie ihn so schnell wie möglich an". Das Makro, das ich oben gesprochen habe, folgt dieser Philosophie.

paercebal
quelle
3
Ja, ABER in Bezug auf Ihr erstes Beispiel könnte das leicht geschrieben werden als: if( !doSomething() ) { puts( "ERROR - doSomething failed" ) ; return ; // or react to failure of doSomething } if( !doSomethingElse() ) { // react to failure of doSomethingElse() }
Bobobobo
Warum nicht ... Aber dann finde ich es immer noch doSomething(); doSomethingElse(); ...besser, denn wenn ich hinzufügen muss, wenn / während / etc. Anweisungen für normale Ausführungszwecke, ich möchte nicht, dass sie mit if / while / etc. gemischt werden. Anweisungen, die für außergewöhnliche Zwecke hinzugefügt wurden ... Und da die eigentliche Regel bei der Verwendung von Ausnahmen darin besteht, zu werfen , nicht zu fangen , sind die try / catch-Anweisungen normalerweise nicht invasiv.
Paercebal
1
Ihr erster Punkt zeigt, wo das Problem mit Ausnahmen liegt. Ihr Kontrollfluss wird seltsam und ist vom eigentlichen Problem getrennt. Es ersetzt einige Identifikationsebenen durch eine Kaskade von Fängen. Ich würde sowohl Rückkehrcodes (oder Objekte mit umfangreichen Informationen) für mögliche Fehler als auch Ausnahmen für Dinge verwenden, die nicht erwartet werden .
Peter
2
@ Peter Weber: Es ist nicht komisch. Es ist vom eigentlichen Problem getrennt, da es nicht Teil des normalen Ausführungsflusses ist. Es ist eine außergewöhnliche Ausführung. Und andererseits geht es bei Ausnahmen um außergewöhnliche Ausnahmen , häufig zu werfen und selten , wenn überhaupt , zu fangen . Daher erscheinen die Catch-Blöcke selten im Code.
Paercebal
Das Beispiel in dieser Art von Debatte ist sehr simpel. Normalerweise führen "doSomething ()" oder "doSomethingElse ()" tatsächlich etwas aus, beispielsweise das Ändern eines Status eines Objekts. Der Ausnahmecode garantiert nicht, dass das Objekt in den vorherigen Zustand zurückversetzt wird, und noch weniger, wenn der Fang sehr weit vom Wurf entfernt ist. Stellen Sie sich beispielsweise vor, doSomething wird zweimal aufgerufen und erhöht einen Zähler vor dem Werfen. Woher wissen Sie, wenn Sie die Ausnahme abfangen, dass Sie ein- oder zweimal dekrementieren sollten? Im Allgemeinen ist das Schreiben von ausnahmesicherem Code für alles, was kein Spielzeugbeispiel ist, sehr schwierig (unmöglich?).
Xryl669
35

Ich benutze eigentlich beide.

Ich verwende Rückkehrcodes, wenn es sich um einen bekannten, möglichen Fehler handelt. Wenn es ein Szenario ist, von dem ich weiß, dass es passieren kann und wird, wird ein Code zurückgesendet.

Ausnahmen werden nur für Dinge verwendet, die ich NICHT erwarte.

Stephen Wrighton
quelle
+1 für einen sehr einfachen Weg. Zumindest ist es kurz genug, damit ich es sehr schnell lesen kann. :-)
Ashish Gupta
1
Ausnahmen werden nur für Dinge verwendet, die ich NICHT erwarte. “ Wenn Sie sie nicht erwarten, warum dann oder wie können Sie sie verwenden?
Anar Khalilov
5
Nur weil ich nicht damit rechne, heißt das nicht, dass ich nicht sehen kann, wie es passieren könnte. Ich erwarte, dass mein SQL Server eingeschaltet ist und reagiert. Trotzdem codiere ich meine Erwartungen so, dass ich bei unerwarteten Ausfallzeiten ordnungsgemäß versagen kann.
Stephen Wrighton
21

Gemäß Kapitel 7 mit dem Titel "Ausnahmen" in den Richtlinien für das Framework-Design: Konventionen, Redewendungen und Muster für wiederverwendbare .NET-Bibliotheken werden zahlreiche Gründe angegeben, warum die Verwendung von Ausnahmen über Rückgabewerte für OO-Frameworks wie C # erforderlich ist.

Vielleicht ist dies der überzeugendste Grund (Seite 179):

"Ausnahmen lassen sich gut in objektorientierte Sprachen integrieren. Objektorientierte Sprachen neigen dazu, Mitgliedssignaturen, die nicht durch Funktionen in Nicht-OO-Sprachen auferlegt werden, Einschränkungen aufzuerlegen. Beispielsweise bei Konstruktoren, Operatorüberladungen und Eigenschaften der Entwickler Der Rückgabewert hat keine Auswahl. Aus diesem Grund ist es nicht möglich, die auf Rückgabewerten basierende Fehlerberichterstattung für objektorientierte Frameworks zu standardisieren. Eine Fehlerberichterstattungsmethode, z. B. Ausnahmen, liegt außerhalb des Bereichs der Methodensignatur ist die einzige Option. "

Hurst
quelle
10

Ich bevorzuge (in C ++ und Python) die Verwendung von Ausnahmen. Die von der Sprache bereitgestellten Funktionen machen es zu einem genau definierten Prozess, Ausnahmen auszulösen, zu fangen und (falls erforderlich) erneut auszulösen, wodurch das Modell einfach zu sehen und zu verwenden ist. Konzeptionell ist es sauberer als Rückkehrcodes, da bestimmte Ausnahmen durch ihre Namen definiert werden können und zusätzliche Informationen beigefügt sind. Mit einem Rückkehrcode sind Sie nur auf den Fehlerwert beschränkt (es sei denn, Sie möchten ein ReturnStatus-Objekt oder etwas anderes definieren).

Sofern der von Ihnen geschriebene Code nicht zeitkritisch ist, ist der mit dem Abwickeln des Stapels verbundene Overhead nicht signifikant genug, um sich Sorgen zu machen.

Jason Etheridge
quelle
2
Denken Sie daran, dass die Verwendung von Ausnahmen die Programmanalyse erschwert.
Paweł Hajdan
7

Ausnahmen sollten nur zurückgegeben werden, wenn etwas passiert, das Sie nicht erwartet haben.

Der andere Ausnahmepunkt in der Vergangenheit ist, dass Rückkehrcodes von Natur aus proprietär sind. Manchmal kann eine 0 von einer C-Funktion zurückgegeben werden, um den Erfolg anzuzeigen, manchmal -1 oder einer von beiden für einen Fehler mit 1 für einen Erfolg. Selbst wenn sie aufgezählt werden, können Aufzählungen mehrdeutig sein.

Ausnahmen können auch viel mehr Informationen liefern und insbesondere gut formulieren: "Etwas ist falsch gelaufen, hier ist was, eine Stapelverfolgung und einige unterstützende Informationen für den Kontext".

Abgesehen davon kann ein gut aufgezählter Rückkehrcode für eine bekannte Reihe von Ergebnissen nützlich sein, ein einfaches "Heres n Ergebnis der Funktion, und es lief einfach so".

Johnc
quelle
6

In Java verwende ich (in der folgenden Reihenfolge):

  1. Design-by-Contract (Sicherstellen, dass die Voraussetzungen erfüllt sind, bevor versucht wird, etwas zu tun , das fehlschlägt). Dies fängt die meisten Dinge ab und ich gebe einen Fehlercode dafür zurück.

  2. Rückgabe von Fehlercodes während der Verarbeitung der Arbeit (und ggf. Durchführen eines Rollbacks).

  3. Ausnahmen, aber diese werden nur für unerwartete Dinge verwendet.

paxdiablo
quelle
1
Wäre es nicht etwas korrekter, Behauptungen für Verträge zu verwenden? Wenn der Vertrag gebrochen ist, gibt es nichts, was Sie retten könnte.
Paweł Hajdan
@ PawełHajdan, Behauptungen sind standardmäßig deaktiviert, glaube ich. Dies hat das gleiche Problem wie das von C, assertda es keine Probleme im Produktionscode auffängt, es sei denn, Sie werden ständig mit Zusicherungen ausgeführt. Ich neige dazu, Behauptungen als einen Weg zu sehen, um Probleme während der Entwicklung zu erkennen, aber nur für Dinge, die sich durchsetzen oder nicht konsistent behaupten (wie Dinge mit Konstanten, keine Dinge mit Variablen oder irgendetwas anderes, das sich zur Laufzeit ändern kann).
paxdiablo
Und zwölf Jahre, um auf Ihre Frage zu antworten. Ich sollte einen Helpdesk betreiben :-)
paxdiablo
6

Rückkehrcodes bestehen den " Pit of Success " -Test für mich fast jedes Mal nicht.

  • Es ist viel zu leicht zu vergessen, einen Rückkehrcode zu überprüfen und später einen Red-Hering-Fehler zu haben.
  • Rückkehrcodes enthalten keine der großartigen Debugging-Informationen wie Aufrufstapel oder innere Ausnahmen.
  • Rückkehrcodes verbreiten sich nicht, was zusammen mit dem obigen Punkt dazu führt, dass übermäßige und miteinander verwobene Diagnoseprotokollierung auftritt, anstatt sich an einem zentralen Ort zu protokollieren (Ausnahmebehandlungsroutinen auf Anwendungs- und Thread-Ebene).
  • Rückkehrcodes neigen dazu, unordentlichen Code in Form von verschachtelten 'if'-Blöcken zu erzeugen
  • Die Entwicklerzeit für das Debuggen eines unbekannten Problems, das sonst eine offensichtliche Ausnahme gewesen wäre (Erfolgsgrube), ist teuer.
  • Wenn das Team hinter C # nicht beabsichtigte, Ausnahmen für den Kontrollfluss zu regeln, würden Ausnahmen nicht eingegeben, es würde keinen "Wann" -Filter für catch-Anweisungen geben und die parameterlose 'throw'-Anweisung wäre nicht erforderlich .

In Bezug auf die Leistung:

  • Ausnahmen können rechenintensiv sein, RELATIV, um überhaupt nicht zu werfen, aber sie werden aus einem bestimmten Grund AUSNAHMEN genannt. Bei Geschwindigkeitsvergleichen wird immer von einer Ausnahmerate von 100% ausgegangen, was niemals der Fall sein sollte. Selbst wenn eine Ausnahme 100x langsamer ist, wie wichtig ist das wirklich, wenn es nur 1% der Zeit passiert?
  • Wenn es sich nicht um Gleitkomma-Arithmetik für Grafikanwendungen oder ähnliches handelt, sind CPU-Zyklen im Vergleich zur Entwicklerzeit günstig.
  • Kosten aus zeitlicher Sicht tragen das gleiche Argument. Im Vergleich zu Datenbankabfragen, Webdienstaufrufen oder Laden von Dateien wird die normale Anwendungszeit die Ausnahmezeit in den Schatten stellen. Die Ausnahmen lagen 2006 fast unter MICROsecond
    • Ich wage jeden, der in .net arbeitet, Ihren Debugger so einzustellen, dass er alle Ausnahmen durchbricht und nur meinen Code deaktiviert, um zu sehen, wie viele Ausnahmen bereits auftreten, von denen Sie noch nicht einmal wissen.
b_levitt
quelle
4

Ein großartiger Ratschlag, den ich von The Pragmatic Programmer erhalten habe, lautete: "Ihr Programm sollte in der Lage sein, alle seine Hauptfunktionen ohne Ausnahmen auszuführen."

Smashery
quelle
4
Du interpretierst es falsch. Was sie meinten war "wenn Ihr Programm Ausnahmen in seinen normalen Flüssen auslöst, ist es falsch". Mit anderen Worten "Verwenden Sie Ausnahmen nur für außergewöhnliche Dinge".
Paweł Hajdan
4

Ich habe vor einiger Zeit einen Blog-Beitrag darüber geschrieben.

Der Leistungsaufwand beim Auslösen einer Ausnahme sollte bei Ihrer Entscheidung keine Rolle spielen. Wenn Sie es richtig machen, ist eine Ausnahme eine Ausnahme .

Thomas
quelle
In dem verlinkten Blog-Beitrag geht es mehr um das Schauen vor dem Sprung (Schecks) als darum, leichter um Vergebung zu bitten als um Erlaubnis (Ausnahmen oder Rückkehrcodes). Ich habe dort mit meinen Gedanken zu diesem Thema geantwortet (Hinweis: TOCTTOU). Bei dieser Frage geht es jedoch um ein anderes Problem, nämlich unter welchen Bedingungen der Ausnahmemechanismus einer Sprache verwendet werden soll, anstatt einen Wert mit speziellen Eigenschaften zurückzugeben.
Damian Yerrick
Ich stimme vollkommen zu. Es scheint, dass ich in den letzten neun Jahren ein oder zwei Dinge gelernt habe;)
Thomas
4

Ich mag keine Rückkehrcodes, weil sie dazu führen, dass das folgende Muster in Ihrem Code auftaucht

CRetType obReturn = CODE_SUCCESS;
obReturn = CallMyFunctionWhichReturnsCodes();
if (obReturn == CODE_BLOW_UP)
{
  // bail out
  goto FunctionExit;
}

Bald wird ein Methodenaufruf, der aus 4 Funktionsaufrufen besteht, mit 12 Zeilen Fehlerbehandlung aufgebläht. Einige davon werden niemals auftreten. Wenn und Schalter Fälle gibt es zuhauf.

Ausnahmen sind sauberer, wenn Sie sie gut verwenden ... um außergewöhnliche Ereignisse zu signalisieren ... nach denen der Ausführungspfad nicht fortgesetzt werden kann. Sie sind oft aussagekräftiger und informativer als Fehlercodes.

Wenn Sie nach einem Methodenaufruf mehrere Zustände haben, die unterschiedlich behandelt werden sollten (und keine Ausnahmefälle sind), verwenden Sie Fehlercodes oder out-Parameter. Obwohl ich persönlich festgestellt habe, dass dies selten ist.

Ich habe ein bisschen über das Gegenargument der Leistungsstrafe gejagt. Mehr in der C ++ / COM-Welt, aber in den neueren Sprachen denke ich, dass der Unterschied nicht so groß ist. In jedem Fall, wenn etwas explodiert, werden Leistungsbedenken in den Hintergrund gedrängt :)

Gishu
quelle
3

Bei anständigen Compiler- oder Laufzeitumgebungen verursachen Ausnahmen keine nennenswerten Nachteile. Es ist mehr oder weniger wie eine GOTO-Anweisung, die zum Ausnahmebehandler springt. Wenn Ausnahmen von einer Laufzeitumgebung (wie der JVM) abgefangen werden, können Sie einen Fehler viel einfacher isolieren und beheben. Ich werde jeden Tag eine NullPointerException in Java über einen Segfault in C durchführen.

Kyle Cronin
quelle
1
Ausnahmen sind extrem teuer. Sie müssen den Stapel durchlaufen, um potenzielle Ausnahmebehandlungsroutinen zu finden. Dieser Stack Walk ist nicht billig. Wenn ein Stack-Trace erstellt wird, ist dieser sogar noch teurer, da dann der gesamte Stack analysiert werden muss .
Derek Park
Ich bin überrascht, dass Compiler zumindest zeitweise nicht bestimmen können, wo die Ausnahme abgefangen wird. Die Tatsache, dass eine Ausnahme den Codefluss verändert, macht es meiner Meinung nach einfacher, genau zu identifizieren, wo ein Fehler auftritt.
Kyle Cronin
Call Stacks können zur Laufzeit sehr komplex werden, und Compiler führen diese Art von Analyse im Allgemeinen nicht durch. Selbst wenn dies der Fall wäre, müssten Sie immer noch über den Stapel laufen, um eine Spur zu erhalten. Sie müssten den Stapel auch noch abwickeln, um mit finallyBlöcken und Destruktoren für vom Stapel zugewiesene Objekte fertig zu werden.
Derek Park
Ich stimme jedoch zu, dass die Debugging-Vorteile von Ausnahmen häufig die Leistungskosten ausgleichen.
Derek Park
Derak Park, Ausnahme sind teuer, wenn sie passieren. Dies ist der Grund, warum sie nicht überbeansprucht werden sollten. Aber wenn sie nicht passieren, kosten sie praktisch nichts.
Paercebal
3

Ich verwende Ausnahmen in Python sowohl unter außergewöhnlichen als auch unter nicht außergewöhnlichen Umständen.

Es ist oft schön, eine Ausnahme verwenden zu können, um anzuzeigen, dass "Anforderung nicht ausgeführt werden konnte", anstatt einen Fehlerwert zurückzugeben. Dies bedeutet, dass Sie / immer / wissen, dass der Rückgabewert der richtige Typ ist, anstatt willkürlich None oder NotFoundSingleton oder so. Hier ist ein gutes Beispiel dafür, wo ich lieber einen Ausnahmebehandler anstelle einer Bedingung für den Rückgabewert verwende.

try:
    dataobj = datastore.fetch(obj_id)
except LookupError:
    # could not find object, create it.
    dataobj = datastore.create(....)

Der Nebeneffekt ist, dass Sie beim Ausführen einer datastore.fetch (obj_id) nie überprüfen müssen, ob der Rückgabewert None ist. Sie erhalten diesen Fehler sofort kostenlos. Dies steht im Widerspruch zu dem Argument "Ihr Programm sollte in der Lage sein, alle Hauptfunktionen ohne Ausnahmen auszuführen".

Hier ist ein weiteres Beispiel dafür, wo Ausnahmen "außergewöhnlich" nützlich sind, um Code für den Umgang mit dem Dateisystem zu schreiben, das nicht den Rennbedingungen unterliegt.

# wrong way:
if os.path.exists(directory_to_remove):
    # race condition is here.
    os.path.rmdir(directory_to_remove)

# right way:
try: 
    os.path.rmdir(directory_to_remove)
except OSError:
    # directory didn't exist, good.
    pass

Ein Systemaufruf statt zwei, keine Rennbedingung. Dies ist ein schlechtes Beispiel, da dies offensichtlich mit einem OSError unter mehr Umständen fehlschlägt, als das Verzeichnis nicht existiert, aber es ist eine "gute" Lösung für viele streng kontrollierte Situationen.

Jerub
quelle
Das zweite Beispiel ist irreführend. Angeblich ist ein falscher Weg falsch, weil der Code os.path.rmdir eine Ausnahme auslösen soll. Die korrekte Implementierung im Rückgabecodefluss wäre 'if rmdir (...) == FAILED: pass'
MaR
3

Ich glaube, dass die Rückkehrcodes das Code-Rauschen verstärken. Zum Beispiel hasste ich das Aussehen von COM / ATL-Code aufgrund von Rückkehrcodes immer. Für jede Codezeile musste eine HRESULT-Prüfung durchgeführt werden. Ich halte den Fehlerrückgabecode für eine der schlechten Entscheidungen der Architekten von COM. Dies macht es schwierig, den Code logisch zu gruppieren, sodass die Codeüberprüfung schwierig wird.

Ich bin mir über den Leistungsvergleich nicht sicher, wenn in jeder Zeile eine explizite Überprüfung des Rückkehrcodes erfolgt.

rpattabi
quelle
COM war so konzipiert, dass es von Sprachen verwendet werden kann, die keine Ausnahmen unterstützen.
Kevin
Das ist ein guter Punkt. Es ist sinnvoll, mit Fehlercodes für Skriptsprachen umzugehen. Zumindest VB6 verbirgt Fehlercodedetails gut, indem sie in das Err-Objekt eingekapselt werden, was bei sauberem Code etwas hilfreich ist.
Rpattabi
Ich bin anderer Meinung: VB6 protokolliert nur den letzten Fehler. In Kombination mit dem berüchtigten "On Error Resume Next" werden Sie die Ursache Ihres Problems vollständig übersehen, sobald Sie es sehen. Beachten Sie, dass dies die Grundlage für die Fehlerbehandlung in Win32 APII ist (siehe GetLastError-Funktion)
paercebal
2

Ich bevorzuge es, Ausnahmen für die Fehlerbehandlung und Rückgabewerte (oder Parameter) als normales Ergebnis einer Funktion zu verwenden. Dies ergibt ein einfaches und konsistentes Fehlerbehandlungsschema, und wenn es richtig gemacht wird, sieht es viel sauberer aus.

Trent
quelle
2

Einer der großen Unterschiede besteht darin, dass Ausnahmen Sie zwingen, einen Fehler zu behandeln, während Fehlerrückgabecodes deaktiviert werden können.

Fehlerrückgabecodes können, wenn sie häufig verwendet werden, auch sehr hässlichen Code mit vielen if-Tests verursachen, die diesem Formular ähnlich sind:

if(function(call) != ERROR_CODE) {
    do_right_thing();
}
else {
    handle_error();
}

Persönlich bevorzuge ich Ausnahmen für Fehler, auf die der aufrufende Code reagieren SOLLTE oder MUSS, und verwende Fehlercodes nur für "erwartete Fehler", bei denen die Rückgabe von etwas tatsächlich gültig und möglich ist.

Daniel Bruce
quelle
Zumindest in C / C ++ und gcc können Sie einer Funktion ein Attribut geben, das eine Warnung generiert, wenn der Rückgabewert ignoriert wird.
Paweł Hajdan
phjr: Obwohl ich mit dem Muster "Rückgabefehlercode" nicht einverstanden bin, sollte Ihr Kommentar vielleicht zu einer vollständigen Antwort werden. Ich finde es interessant genug. Zumindest gab es mir nützliche Informationen.
Paercebal
2

Ich habe ein einfaches Regelwerk:

1) Verwenden Sie Rückkehrcodes für Dinge, auf die Ihr sofortiger Anrufer reagieren soll.

2) Verwenden Sie Ausnahmen für Fehler, deren Umfang breiter ist und von denen vernünftigerweise erwartet werden kann, dass sie von vielen Ebenen über dem Aufrufer behandelt werden, damit die Fehlererkennung nicht über viele Ebenen verteilt werden muss, wodurch der Code komplexer wird.

In Java habe ich immer nur ungeprüfte Ausnahmen verwendet, geprüfte Ausnahmen sind nur eine andere Form des Rückkehrcodes, und meiner Erfahrung nach war die Dualität dessen, was durch einen Methodenaufruf "zurückgegeben" werden könnte, im Allgemeinen eher ein Hindernis als eine Hilfe.

Kendall Helmstetter Gelner
quelle
2

Es gibt viele Gründe, Ausnahmen dem Rückkehrcode vorzuziehen:

  • Normalerweise versuchen Benutzer aus Gründen der Lesbarkeit, die Anzahl der return-Anweisungen in einer Methode zu minimieren. Auf diese Weise verhindern Ausnahmen, dass im Incoorect-Zustand zusätzliche Arbeit geleistet wird, und verhindern somit, dass möglicherweise mehr Daten beschädigt werden.
  • Ausnahmen sind im Allgemeinen ausführlicher und einfacher erweiterbar als der Rückgabewert. Angenommen, eine Methode gibt eine natürliche Zahl zurück und Sie verwenden negative Zahlen als Rückkehrcode, wenn ein Fehler auftritt. Wenn sich der Umfang Ihrer Methode ändert und jetzt Ganzzahlen zurückgibt, müssen Sie alle Methodenaufrufe ändern, anstatt nur ein wenig zu optimieren die Ausnahme.
  • Ausnahmen ermöglichen eine einfachere Trennung der Fehlerbehandlung von normalem Verhalten. Sie ermöglichen es sicherzustellen, dass einige Operationen irgendwie als atomare Operation ausgeführt werden.
Gizmo
quelle
2

Ausnahmen gelten nicht für die Fehlerbehandlung, IMO. Ausnahmen sind genau das; außergewöhnliche Ereignisse, die Sie nicht erwartet hatten. Verwenden Sie mit Vorsicht, sage ich.

Fehlercodes können in Ordnung sein, aber die Rückgabe von 404 oder 200 von einer Methode ist schlecht, IMO. Verwenden Sie stattdessen Aufzählungen (.Net), um den Code lesbarer und für andere Entwickler einfacher zu verwenden. Außerdem müssen Sie keine Tabelle über Zahlen und Beschreibungen führen.

Ebenfalls; Das Try-Catch-Endlich-Muster ist ein Anti-Muster in meinem Buch. Try-Catch kann gut sein, Try-Catch kann auch gut sein, aber Try-Catch-Endlich ist nie gut. try-finally kann oft durch eine "using" -Anweisung (IDispose-Muster) ersetzt werden, was IMO besser ist. Und Try-Catch, bei dem Sie tatsächlich eine Ausnahme abfangen, die Sie behandeln können, ist gut, oder wenn Sie dies tun:

try{
    db.UpdateAll(somevalue);
}
catch (Exception ex) {
    logger.Exception(ex, "UpdateAll method failed");
    throw;
}

Solange Sie die Ausnahme weiter sprudeln lassen, ist dies in Ordnung. Ein weiteres Beispiel ist folgendes:

try{
    dbHasBeenUpdated = db.UpdateAll(somevalue); // true/false
}
catch (ConnectionException ex) {
    logger.Exception(ex, "Connection failed");
    dbHasBeenUpdated = false;
}

Hier behandle ich tatsächlich die Ausnahme; Was ich außerhalb des Try-Catch mache, wenn die Update-Methode fehlschlägt, ist eine andere Geschichte, aber ich denke, mein Punkt wurde klargestellt. :) :)

Warum ist Try-Catch-Endlich dann ein Anti-Pattern? Hier ist der Grund:

try{
    db.UpdateAll(somevalue);
}
catch (Exception ex) {
    logger.Exception(ex, "UpdateAll method failed");
    throw;
}
finally {
    db.Close();
}

Was passiert, wenn das Datenbankobjekt bereits geschlossen wurde? Eine neue Ausnahme wird ausgelöst und muss behandelt werden! Das ist besser:

try{
    using(IDatabase db = DatabaseFactory.CreateDatabase()) {
        db.UpdateAll(somevalue);
    }
}
catch (Exception ex) {
    logger.Exception(ex, "UpdateAll method failed");
    throw;
}

Wenn das Datenbankobjekt IDisposable nicht implementiert, gehen Sie folgendermaßen vor:

try{
    try {
        IDatabase db = DatabaseFactory.CreateDatabase();
        db.UpdateAll(somevalue);
    }
    finally{
        db.Close();
    }
}
catch (DatabaseAlreadyClosedException dbClosedEx) {
    logger.Exception(dbClosedEx, "Database connection was closed already.");
}
catch (Exception ex) {
    logger.Exception(ex, "UpdateAll method failed");
    throw;
}

Das sind sowieso meine 2 Cent! :) :)

Noozyten
quelle
Es wird seltsam sein, wenn ein Objekt .Close () hat, aber keine .Dispose ()
abatishchev
Ich verwende nur Close () als Beispiel. Fühlen Sie sich frei, es als etwas anderes zu betrachten. Wie ich sage; Das Verwendungsmuster sollte verwendet werden (doh!), falls verfügbar. Dies impliziert natürlich, dass die Klasse IDisposable implementiert und Sie als solche Dispose aufrufen können.
Noocyte
1

Ich benutze nur Ausnahmen, keine Rückkehrcodes. Ich spreche hier von Java.

Die allgemeine Regel, der ich folge, lautet: Wenn ich eine Methode habe, die aufgerufen doFoo()wird, folgt, dass, wenn sie sozusagen nicht "do foo" ist, etwas Außergewöhnliches passiert ist und eine Ausnahme ausgelöst werden sollte.

SCdF
quelle
1

Eine Sache, die ich bei Ausnahmen befürchte, ist, dass das Auslösen einer Ausnahme den Codefluss beeinträchtigt. Zum Beispiel, wenn Sie dies tun

void foo()
{
  MyPointer* p = NULL;
  try{
    p = new PointedStuff();
    //I'm a module user and  I'm doing stuff that might throw or not

  }
  catch(...)
  {
    //should I delete the pointer?
  }
}

Oder noch schlimmer, wenn ich etwas löschte, das ich nicht hätte haben sollen, aber geworfen wurde, um es zu fangen, bevor ich den Rest der Bereinigung erledigte. Werfen hat meiner Meinung nach viel Gewicht auf den armen Benutzer gelegt.

Robert Gould
quelle
Dafür ist die Anweisung <code> finally </ code> gedacht. Aber leider ist es nicht im C ++ Standard ...
Thomas
In C ++ sollten Sie sich an die Faustregel halten: "Erwerben Sie Ressourcen in construcotor und geben Sie sie in destrucotor frei. In diesem speziellen Fall funktioniert auto_ptr nur perfekt.
Serge
Thomas, du liegst falsch. C ++ hat nicht endlich, weil es es nicht braucht. Es hat stattdessen RAII. Die Lösung von Serge ist eine Lösung mit RAII.
Paercebal
Robert, benutze Serges Lösung und du wirst feststellen, dass dein Problem weg ist. Wenn Sie nun mehr Versuche / Fänge als Würfe schreiben, haben Sie (anhand Ihres Kommentars) möglicherweise ein Problem in Ihrem Code. Natürlich ist die Verwendung von catch (...) ohne erneuten Wurf normalerweise schlecht, da der Fehler ausgeblendet wird, um ihn besser zu ignorieren.
Paercebal
1

Meine allgemeine Regel im Argument "Ausnahme vs. Rückkehrcode":

  • Verwenden Sie Fehlercodes, wenn Sie eine Lokalisierung / Internationalisierung benötigen. In .NET können Sie diese Fehlercodes verwenden, um auf eine Ressourcendatei zu verweisen, die den Fehler dann in der entsprechenden Sprache anzeigt. Verwenden Sie andernfalls Ausnahmen
  • Verwenden Sie Ausnahmen nur für Fehler, die wirklich außergewöhnlich sind. Wenn dies ziemlich häufig vorkommt, verwenden Sie entweder einen Booleschen oder einen Enum-Fehlercode.
Jon Limjap
quelle
Es gibt keinen Grund, obwohl Sie keine Ausnahme verwenden könnten, wenn Sie l10n / i18n ausführen. Ausnahmen können auch lokalisierte Informationen enthalten.
Gizmo
1

Ich finde Rückkehrcodes nicht weniger hässlich als Ausnahmen. Mit der Ausnahme haben Sie das try{} catch() {} finally {}Wo wie bei den Rückkehrcodes, die Sie haben if(){}. Ich habe aus den in der Post angegebenen Gründen Ausnahmen befürchtet. Sie wissen nicht, ob der Zeiger gelöscht werden muss, was haben Sie. Aber ich denke, Sie haben die gleichen Probleme, wenn es um die Rückkehrcodes geht. Sie kennen den Status der Parameter nur, wenn Sie einige Details zu der betreffenden Funktion / Methode kennen.

Unabhängig davon müssen Sie den Fehler nach Möglichkeit behandeln. Sie können eine Ausnahme genauso einfach auf die oberste Ebene übertragen lassen, wie einen Rückkehrcode ignorieren und das Programm segfault lassen.

Ich mag die Idee, einen Wert (Aufzählung?) Für Ergebnisse und eine Ausnahme für einen Ausnahmefall zurückzugeben.

Jonathan Adelson
quelle
0

Für eine Sprache wie Java würde ich Exception verwenden, da der Compiler einen Fehler bei der Kompilierungszeit ausgibt, wenn Ausnahmen nicht behandelt werden. Dadurch wird die aufrufende Funktion gezwungen, die Ausnahmen zu behandeln / auszulösen.

Für Python bin ich mehr in Konflikt geraten. Es gibt keinen Compiler, daher ist es möglich, dass der Aufrufer die von der Funktion ausgelöste Ausnahme, die zu Laufzeitausnahmen führt, nicht verarbeitet. Wenn Sie Rückkehrcodes verwenden, kann es zu unerwartetem Verhalten kommen, wenn diese nicht ordnungsgemäß behandelt werden. Wenn Sie Ausnahmen verwenden, können Laufzeitausnahmen auftreten.

2ank3th
quelle
0

Im Allgemeinen bevorzuge ich Rückkehrcodes, da der Anrufer entscheiden kann, ob der Fehler außergewöhnlich ist .

Dieser Ansatz ist typisch für die Elixiersprache.

# I care whether this succeeds. If it doesn't return :ok, raise an exception.
:ok = File.write(path, content)

# I don't care whether this succeeds. Don't check the return value.
File.write(path, content)

# This had better not succeed - the path should be read-only to me.
# If I get anything other than this error, raise an exception.
{:error, :erofs} = File.write(path, content)

# I want this to succeed but I can handle its failure
case File.write(path, content) do
  :ok => handle_success()
  error => handle_error(error)
end

Die Leute erwähnten, dass Rückkehrcodes dazu führen können, dass Sie viele verschachtelte ifAnweisungen haben, aber dies kann mit einer besseren Syntax behandelt werden. In Elixir withkönnen wir mit der Anweisung auf einfache Weise eine Reihe von Happy-Path-Rückgabewerten von Fehlern trennen.

with {:ok, content} <- get_content(),
  :ok <- File.write(path, content) do
    IO.puts "everything worked, happy path code goes here"
else
  # Here we can use a single catch-all failure clause
  # or match every kind of failure individually
  # or match subsets of them however we like
  _some_error => IO.puts "one of those steps failed"
  _other_error => IO.puts "one of those steps failed"
end

Elixir hat immer noch Funktionen, die Ausnahmen auslösen. Zurück zu meinem ersten Beispiel: Ich könnte beides tun, um eine Ausnahme auszulösen, wenn die Datei nicht geschrieben werden kann.

# Raises a generic MatchError because the return value isn't :ok
:ok = File.write(path, content)

# Raises a File.Error with a descriptive error message - eg, saying
# that the file is read-only
File.write!(path, content)

Wenn ich als Anrufer weiß, dass ich einen Fehler auslösen möchte, wenn der Schreibvorgang fehlschlägt, kann ich File.write!anstelle von anrufen File.write. Oder ich kann wählen, ob ich File.writejeden der möglichen Fehlergründe anders anrufen und behandeln möchte.

Natürlich ist es immer zu rescueeiner Ausnahme möglich, wenn wir wollen. Aber im Vergleich zum Umgang mit einem informativen Rückgabewert erscheint es mir unangenehm. Wenn ich weiß, dass ein Funktionsaufruf fehlschlagen kann oder sogar fehlschlagen sollte, ist sein Fehler kein Ausnahmefall.

Nathan Long
quelle