Ich benutze Ausnahmen, um Probleme frühzeitig zu erkennen. Beispielsweise:
public int getAverageAge(Person p1, Person p2){
if(p1 == null || p2 == null)
throw new IllegalArgumentException("One or more of input persons is null").
return (p1.getAge() + p2.getAge()) / 2;
}
Mein Programm sollte null
diese Funktion niemals bestehen . Ich habe es nie vor. Wie wir alle wissen, passiert jedoch unbeabsichtigtes Programmieren.
Wenn dieses Problem auftritt, kann ich eine Ausnahme auslösen und beheben, bevor an anderen Stellen im Programm weitere Probleme auftreten. Die Ausnahme stoppt das Programm und sagt mir, dass "hier Schlimmes passiert ist, behebe es". Anstatt null
sich im Programm zu bewegen und Probleme zu verursachen.
Nun, Sie haben Recht, in diesem Fall null
würde das einfach NullPointerException
sofort einen auslösen , daher ist es möglicherweise nicht das beste Beispiel.
Aber betrachten Sie eine Methode wie diese zum Beispiel:
public void registerPerson(Person person){
persons.add(person);
notifyRegisterObservers(person); // sends the person object to all kinds of objects.
}
In diesem Fall würde ein null
as-Parameter um das Programm herum übergeben und könnte später zu Fehlern führen, die nur schwer auf ihren Ursprung zurückzuführen sind.
Ändern Sie die Funktion wie folgt:
public void registerPerson(Person person){
if(person == null) throw new IllegalArgumentException("Input person is null.");
persons.add(person);
notifyRegisterObservers(person); // sends the person object to all kinds of objects.
}
Ermöglicht es mir, das Problem zu erkennen, bevor es an anderen Stellen seltsame Fehler verursacht.
Auch eine null
Referenz als Parameter ist nur ein Beispiel. Es kann viele Arten von Problemen geben, von ungültigen Argumenten bis zu allem anderen. Es ist immer besser, sie früh zu erkennen.
Meine Frage lautet also einfach: Ist das eine gute Praxis? Ist meine Verwendung von Ausnahmen als Tools zur Problemverhütung sinnvoll? Ist dies eine legitime Anwendung von Ausnahmen oder ist dies problematisch?
quelle
Antworten:
Ja, "Früh scheitern" ist ein sehr gutes Prinzip, und dies ist einfach eine Möglichkeit, es umzusetzen. Und in Methoden , die einen bestimmten Wert zurückkommen müssen, ist es nicht wirklich sehr viel , was Sie können absichtlich nicht tun - es ist entweder Ausnahmen werfen, oder Auslösen Behauptungen. Ausnahmen sollen "außergewöhnliche" Zustände signalisieren, und das Erkennen eines Programmierfehlers ist sicherlich außergewöhnlich.
quelle
Ja, es ist eine gute Idee, Ausnahmen zu werfen. Wirf sie früh, wirf sie oft, wirf sie eifrig.
Ich weiß, dass es eine Debatte zwischen "Ausnahmen und Zusicherungen" gibt, bei der einige außergewöhnliche Verhaltensweisen (insbesondere solche, die Programmierfehler widerspiegeln) von Zusicherungen behandelt werden, die zur Laufzeit "kompiliert" werden können, anstatt Builds zu debuggen / testen. Die Leistung, die bei einigen zusätzlichen Korrektheitsprüfungen verbraucht wird, ist jedoch bei moderner Hardware minimal, und die zusätzlichen Kosten werden bei weitem durch den Wert der Erzielung korrekter, fehlerfreier Ergebnisse aufgewogen. Ich habe noch nie eine Anwendungscodebasis getroffen, für die (die meisten) Prüfungen zur Laufzeit entfernt werden sollen.
Ich bin versucht zu sagen, dass ich nicht viele zusätzliche Überprüfungen und Bedingungen in den engen Schleifen des numerisch intensiven Codes haben möchte ... aber hier werden tatsächlich viele numerische Fehler erzeugt, und wenn sie dort nicht erfasst werden, breiten sie sich nach außen aus alle Ergebnisse beeinflussen. Schecks lohnen sich also auch dort. Tatsächlich basieren einige der besten und effizientesten numerischen Algorithmen auf der Fehlerbewertung.
Ein letzter Punkt, bei dem Sie sich des zusätzlichen Codes bewusst sein sollten, ist der Code, der sehr latenzempfindlich ist und bei dem zusätzliche Bedingungen zum Stillstand der Pipeline führen können. In der Mitte des Betriebssystems, des DBMS und anderer Middleware-Kernel sowie der Kommunikation / Protokollverarbeitung auf niedriger Ebene. Aber auch dies sind einige der Orte, an denen Fehler am wahrscheinlichsten beobachtet werden und deren Auswirkungen (Sicherheit, Korrektheit und Datenintegrität) am schädlichsten sind.
Eine meiner Verbesserungen besteht darin, nicht nur Ausnahmen auf Basisebene auszulösen.
IllegalArgumentException
ist gut, kann aber im wesentlichen von überall her kommen. In den meisten Sprachen ist nicht viel erforderlich, um benutzerdefinierte Ausnahmen hinzuzufügen. Sagen Sie für Ihr Personenhandhabungsmodul:Dann, wenn jemand eine sieht
PersonArgumentException
Es ist klar, woher es kommt. Es gibt einen Balanceakt darüber, wie viele benutzerdefinierte Ausnahmen Sie hinzufügen möchten, da Sie Entitäten nicht unnötig multiplizieren möchten (Occam's Razor). Oft genügen nur ein paar benutzerdefinierte Ausnahmen, um zu signalisieren, dass dieses Modul nicht die richtigen Daten erhält! oder "Dieses Modul kann nicht das tun, was es tun soll!" auf eine Weise, die spezifisch und maßgeschneidert ist, aber nicht so präzise, dass Sie die gesamte Ausnahmehierarchie erneut implementieren müssen. Ich komme oft zu diesem kleinen Satz von benutzerdefinierten Ausnahmen, indem ich mit Ausnahmen aus dem Lagerbestand beginne, dann den Code scanne und merke, dass "diese N Stellen Ausnahmen aus dem Lagerbestand auslösen, aber sie laufen auf die übergeordnete Idee hinaus, dass sie nicht die Daten erhalten sie brauchen, lass 'quelle
I've never actually met an application codebase for which I'd want (most) checks removed at runtime.
Dann haben Sie keinen leistungskritischen Code erstellt. Ich arbeite gerade an etwas, das 37 Millionen Operationen pro Sekunde mit Behauptungen ausführt, die in und 42 Millionen ohne Behauptungen kompiliert wurden. Die Behauptungen sind nicht da, um externe Eingaben zu validieren, sondern um sicherzustellen, dass der Code korrekt ist. Meine Kunden freuen sich über die 13% ige Steigerung, wenn ich zufrieden bin, dass meine Sachen nicht kaputt sind.PersonArgumentException
ist das nicht so klar wieIllegalArgumentException
. Es ist allgemein bekannt, dass Letzteres geworfen wird, wenn ein rechtswidriges Argument übergeben wird. Ich würde eigentlich erwarten, dass Ersteres ausgelöst wird, wenn sich aPerson
für einen Anruf in einem ungültigen Zustand befindet (ähnlich wieInvalidOperationException
in C #).Wenn Sie eine Anwendung debuggen, ist es sehr hilfreich, wenn Sie so schnell wie möglich fehlschlagen. Ich erinnere mich an einen bestimmten Segmentierungsfehler in einem älteren C ++ - Programm: Der Ort, an dem der Fehler entdeckt wurde, hatte nichts mit dem Ort zu tun, an dem er eingeführt wurde (der Nullzeiger wurde glücklich von einem Ort zum anderen im Speicher verschoben, bevor er schließlich ein Problem verursachte ). Stapelspuren können Ihnen in solchen Fällen nicht weiterhelfen.
Defensives Programmieren ist also ein sehr effektiver Ansatz, um Fehler schnell zu erkennen und zu beheben. Andererseits kann es übertrieben werden, insbesondere bei Null-Referenzen.
In Ihrem speziellen Fall zum Beispiel: Wenn einer der Verweise null ist,
NullReferenceException
wird der bei der nächsten Anweisung geworfen, wenn versucht wird, das Alter einer Person zu ermitteln. Sie müssen die Dinge hier nicht wirklich selbst überprüfen: Lassen Sie das zugrunde liegende System diese Fehler abfangen und Ausnahmen auslösen, deshalb gibt es sie .Für ein realistischeres Beispiel können Sie
assert
Anweisungen verwenden, die:Sind kürzer zu schreiben und zu lesen:
Sind speziell für Ihren Ansatz konzipiert. In einer Welt, in der Sie sowohl Behauptungen als auch Ausnahmen haben, können Sie diese wie folgt unterscheiden:
Wenn Sie also Ihre Annahmen über die Eingaben und / oder den Status Ihrer Anwendung mit Behauptungen offenlegen, kann der nächste Entwickler den Zweck Ihres Codes ein wenig besser verstehen.
Ein statischer Analysator (z. B. der Compiler) ist möglicherweise auch zufriedener.
Schließlich können Zusicherungen mit einem einzigen Schalter aus der bereitgestellten Anwendung entfernt werden. Erwarten Sie damit aber im Allgemeinen keine Effizienzsteigerung: Assertions-Checks zur Laufzeit sind vernachlässigbar.
quelle
Soweit ich weiß, bevorzugen verschiedene Programmierer die eine oder andere Lösung.
Die erste Lösung wird in der Regel bevorzugt, da sie übersichtlicher ist. Insbesondere müssen Sie nicht immer wieder denselben Zustand in verschiedenen Funktionen überprüfen.
Ich finde die zweite Lösung, z
fester, weil
registerPerson()
aufgerufen wird, und nicht, wenn eine Nullzeigerausnahme irgendwo im Aufrufstapel abgelegt wird. Das Debuggen wird viel einfacher: Wir alle wissen, wie weit ein ungültiger Wert durch den Code wandern kann, bevor er sich als Fehler manifestiert.registerPerson()
keine Annahmen darüber getroffen, welche anderen Funktionen dasperson
Argument verwenden und wie sie es verwenden: Die Entscheidung, bei dernull
es sich um einen Fehler handelt, wird lokal getroffen und implementiert.Insbesondere wenn der Code ziemlich komplex ist, bevorzuge ich diesen zweiten Ansatz.
quelle
Im Allgemeinen ist es eine gute Idee, "früh zu scheitern". In Ihrem speziellen Beispiel bietet der explizite
IllegalArgumentException
Code jedoch keine signifikante Verbesserung gegenüber aNullReferenceException
-, da beide Objekte, mit denen gearbeitet wird, bereits als Argumente an die Funktion übergeben werden.Aber schauen wir uns ein etwas anderes Beispiel an.
Wenn der Konstruktor kein Argument enthält, wird
NullReferenceException
beim Aufruf ein angezeigtCalculate
.Aber das kaputte Stück Code war weder die
Calculate
Funktion noch der Konsument derCalculate
Funktion. Das kaputte Stück Code war der Code, der versucht, dasPersonCalculator
mit einer Null zu konstruierenPerson
- also möchten wir, dass die Ausnahme auftritt.Wenn wir diese explizite Argumentprüfung entfernen, müssen Sie herausfinden, warum ein
NullReferenceException
beimCalculate
Aufruf von aufgetreten ist . Das Auffinden, warum das Objekt mit einernull
Person erstellt wurde, kann schwierig werden, insbesondere wenn der Code, der den Taschenrechner erstellt, nicht in der Nähe des Codes liegt, der dieCalculate
Funktion tatsächlich aufruft .quelle
Nicht in den Beispielen, die Sie geben.
Wie Sie sagen, bringt Ihnen das explizite Werfen nicht viel, wenn Sie kurz darauf eine Ausnahme machen. Viele werden argumentieren, dass es besser ist, die explizite Ausnahme mit einer guten Botschaft zu haben, obwohl ich nicht einverstanden bin. In vorab veröffentlichten Szenarien ist der Stack-Trace ausreichend. In Post-Release-Szenarien kann der Anrufstandort häufig bessere Nachrichten als innerhalb der Funktion bereitstellen.
Das zweite Formular enthält zu viele Informationen zur Funktion. Diese Funktion sollte nicht unbedingt wissen, dass die anderen Funktionen eine Null-Eingabe auslösen. Selbst wenn sie Wurf auf null Eingabe tun jetzt , es zu refactor sehr lästig wird sollte das Stop der Fall , da die Nullprüfung ist Ausbreitung über den gesamten Code zu sein.
Aber im Allgemeinen sollten Sie früh werfen, wenn Sie feststellen, dass etwas schief gelaufen ist (unter Berücksichtigung von DRY). Dies sind jedoch vielleicht keine großartigen Beispiele dafür.
quelle
In Ihrer Beispielfunktion wäre es mir lieber, wenn Sie keine Überprüfungen durchführen und nur zulassen
NullReferenceException
, dass dies geschieht.Zum einen macht es keinen Sinn, dort sowieso eine Null zu übergeben, also werde ich das Problem sofort anhand des Werfens von a herausfinden
NullReferenceException
.Zweitens: Wenn jede Funktion aufgrund der Art der offensichtlich falschen Eingaben leicht unterschiedliche Ausnahmen auslöst, gibt es bald Funktionen, die möglicherweise 18 verschiedene Arten von Ausnahmen auslösen, und bald werden Sie feststellen, dass dies auch der Fall ist viel arbeit zu erledigen und einfach alle ausnahmen sowieso zu unterdrücken.
Es gibt nichts, was Sie wirklich tun können, um die Situation eines Entwurfszeitfehlers in Ihrer Funktion zu verbessern. Lassen Sie den Fehler also einfach geschehen, ohne ihn zu ändern.
quelle
RuntimeException
nicht unbedingt eine gültige Annahme.