Sie sollten beide verwenden. Die Sache ist zu entscheiden, wann jeder verwendet werden soll .
Es gibt einige Szenarien, in denen Ausnahmen die offensichtliche Wahl sind :
In einigen Situationen können Sie mit dem Fehlercode nichts anfangen , und Sie müssen ihn nur auf einer höheren Ebene im Aufrufstapel behandeln , normalerweise nur den Fehler protokollieren, dem Benutzer etwas anzeigen oder das Programm schließen. In diesen Fällen müssten Sie bei Fehlercodes die Fehlercodes manuell Stufe für Stufe aufblasen, was mit Ausnahmen offensichtlich viel einfacher ist. Der Punkt ist, dass dies für unerwartete und nicht handhabbare Situationen ist.
In Bezug auf Situation 1 (in der etwas Unerwartetes und Unhandhabbares passiert, das Sie einfach nicht protokollieren möchten) können Ausnahmen hilfreich sein, da Sie möglicherweise Kontextinformationen hinzufügen . Wenn ich beispielsweise eine SqlException in meinen untergeordneten Datenhelfern erhalte, möchte ich diesen Fehler in der untergeordneten Ebene abfangen (wo ich den SQL-Befehl kenne, der den Fehler verursacht hat), damit ich diese Informationen erfassen und mit zusätzlichen Informationen erneut werfen kann . Bitte beachten Sie hier das Zauberwort: neu werfen und nicht schlucken .
Die erste Regel für die Ausnahmebehandlung: Schlucken Sie keine Ausnahmen . Beachten Sie außerdem, dass mein innerer Fang nichts protokollieren muss, da der äußere Fang den gesamten Stack-Trace enthält und diesen möglicherweise protokolliert.
In einigen Situationen haben Sie eine Folge von Befehlen, und wenn einer von ihnen fehlschlägt , sollten Sie Ressourcen bereinigen / entsorgen (*), unabhängig davon, ob es sich um eine nicht behebbare Situation handelt (die ausgelöst werden sollte) oder um eine wiederherstellbare Situation (in diesem Fall können Sie dies lokal oder im Anrufercode behandeln, aber Sie brauchen keine Ausnahmen). Offensichtlich ist es viel einfacher, alle diese Befehle in einem einzigen Versuch zu platzieren, anstatt Fehlercodes nach jeder Methode zu testen und im finally-Block zu bereinigen / zu entsorgen. Bitte beachten Sie, dass Sie , wenn Sie möchten, dass der Fehler in die Luft sprudelt (was wahrscheinlich das ist, was Sie wollen), ihn nicht einmal abfangen müssen - Sie verwenden nur das endgültige zum Bereinigen / Entsorgen - Sie sollten catch / retrow nur verwenden, wenn Sie möchten Kontextinformationen hinzufügen (siehe Punkt 2).
Ein Beispiel wäre eine Folge von SQL-Anweisungen innerhalb eines Transaktionsblocks. Auch dies ist eine "unhandhabbare" Situation, selbst wenn Sie sich entschließen, sie frühzeitig zu fangen (lokal behandeln, anstatt nach oben zu sprudeln). Es ist immer noch eine fatale Situation, in der das beste Ergebnis darin besteht, alles abzubrechen oder zumindest einen großen abzubrechen Teil des Prozesses.
(*) Dies ist wie das on error goto
, was wir in alten Visual Basic verwendet haben
In Konstruktoren können Sie nur Ausnahmen auslösen.
In allen anderen Situationen, in denen Sie Informationen zurückgeben, für die der Anrufer Maßnahmen ergreifen kann / sollte , ist die Verwendung von Rückkehrcodes wahrscheinlich die bessere Alternative. Dies schließt alle erwarteten "Fehler" ein , da sie wahrscheinlich vom unmittelbaren Aufrufer behandelt werden sollten und kaum zu viele Ebenen im Stapel hochgeblasen werden müssen.
Natürlich ist es immer möglich, erwartete Fehler als Ausnahmen zu behandeln und dann sofort eine Ebene darüber abzufangen, und es ist auch möglich, jede Codezeile in einem Try-Catch zu erfassen und für jeden möglichen Fehler Maßnahmen zu ergreifen. IMO, dies ist ein schlechtes Design, nicht nur, weil es viel ausführlicher ist, sondern insbesondere, weil die möglichen Ausnahmen, die möglicherweise ausgelöst werden, ohne Lesen des Quellcodes nicht offensichtlich sind - und Ausnahmen von jeder tiefen Methode ausgelöst werden können, wodurch unsichtbare Gotos entstehen . Sie unterbrechen die Codestruktur, indem sie mehrere unsichtbare Austrittspunkte erstellen, die das Lesen und Überprüfen von Code erschweren. Mit anderen Worten, Sie sollten niemals Ausnahmen als Flusskontrolle verwenden, weil das für andere schwer zu verstehen und aufrechtzuerhalten wäre. Es kann sogar schwierig werden, alle möglichen Codeflüsse zum Testen zu verstehen.
Nochmals: Für eine korrekte Bereinigung / Entsorgung können Sie try-finally verwenden, ohne etwas zu fangen .
Die populärste Kritik an Rückkehrcodes ist, dass "jemand die Fehlercodes ignorieren könnte, aber im gleichen Sinne kann jemand auch Ausnahmen verschlucken. Eine schlechte Ausnahmebehandlung ist bei beiden Methoden einfach . Das Schreiben eines guten fehlercodebasierten Programms ist jedoch immer noch viel einfacher als ein ausnahmebasiertes Programm zu schreiben . Und wenn einer aus irgendeinem Grund beschließt, alle Fehler (die alten on error resume next
) zu ignorieren , können Sie dies leicht mit Rückkehrcodes tun, und Sie können dies nicht ohne viele Try-Catches tun.
Die zweitbeliebteste Kritik an Rückkehrcodes ist, dass "es schwierig ist, in die Luft zu sprudeln" - aber das liegt daran, dass die Leute nicht verstehen, dass Ausnahmen für nicht wiederherstellbare Situationen gelten, während Fehlercodes dies nicht tun.
Die Entscheidung zwischen Ausnahmen und Fehlercodes ist eine Grauzone. Es ist sogar möglich, dass Sie einen Fehlercode von einer wiederverwendbaren Geschäftsmethode erhalten müssen, und dann entscheiden Sie sich, diesen in eine Ausnahme zu packen (möglicherweise Informationen hinzuzufügen) und ihn in die Luft sprudeln zu lassen. Es ist jedoch ein Konstruktionsfehler anzunehmen, dass ALLE Fehler als Ausnahmen ausgelöst werden sollten.
Etwas zusammenfassen:
Ich verwende gerne Ausnahmen, wenn ich eine unerwartete Situation habe, in der nicht viel zu tun ist, und normalerweise möchten wir einen großen Codeblock oder sogar die gesamte Operation oder das gesamte Programm abbrechen. Dies ist wie das alte "on error goto".
Ich verwende gerne Rückkehrcodes, wenn ich Situationen erwartet habe, in denen der Anrufercode Maßnahmen ergreifen kann / sollte. Dies umfasst die meisten Geschäftsmethoden, APIs, Validierungen usw.
Dieser Unterschied zwischen Ausnahmen und Fehlercodes ist eines der Entwurfsprinzipien der GO-Sprache, die "Panik" für schwerwiegende unerwartete Situationen verwendet, während regelmäßig erwartete Situationen als Fehler zurückgegeben werden.
Bei GO sind jedoch auch mehrere Rückgabewerte zulässig , was bei der Verwendung von Rückkehrcodes sehr hilfreich ist, da Sie gleichzeitig einen Fehler und etwas anderes zurückgeben können. Unter C # / Java können wir dies ohne Parameter, Tupel oder (meine Lieblings-) Generika erreichen, die in Kombination mit Aufzählungen dem Aufrufer eindeutige Fehlercodes liefern können:
public MethodResult<CreateOrderResultCodeEnum, Order> CreateOrder(CreateOrderOptions options)
{
....
return MethodResult<CreateOrderResultCodeEnum>.CreateError(CreateOrderResultCodeEnum.NO_DELIVERY_AVAILABLE, "There is no delivery service in your area");
...
return MethodResult<CreateOrderResultCodeEnum>.CreateSuccess(CreateOrderResultCodeEnum.SUCCESS, order);
}
var result = CreateOrder(options);
if (result.ResultCode == CreateOrderResultCodeEnum.OUT_OF_STOCK)
// do something
else if (result.ResultCode == CreateOrderResultCodeEnum.SUCCESS)
order = result.Entity; // etc...
Wenn ich meiner Methode eine neue mögliche Rückgabe hinzufüge, kann ich sogar alle Aufrufer überprüfen, ob sie diesen neuen Wert beispielsweise in einer switch-Anweisung abdecken. Mit Ausnahmen kann man das wirklich nicht machen. Wenn Sie Rückkehrcodes verwenden, kennen Sie normalerweise alle möglichen Fehler im Voraus und testen sie. Mit Ausnahmen wissen Sie normalerweise nicht, was passieren könnte. Das Umschließen von Aufzählungen in Ausnahmen (anstelle von Generika) ist eine Alternative (solange klar ist, welche Art von Ausnahmen jede Methode auslösen wird), aber IMO ist das Design immer noch schlecht.