Ist es in Ordnung, Ausnahmen als Tools zu verwenden, um Fehler frühzeitig zu erkennen?

29

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 nulldiese 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 nullsich im Programm zu bewegen und Probleme zu verursachen.

Nun, Sie haben Recht, in diesem Fall nullwürde das einfach NullPointerExceptionsofort 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 nullas-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 nullReferenz 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?

Aviv Cohn
quelle
2
Kann der Downvoter erklären, was mit dieser Frage nicht stimmt?
Giorgio
2
"Früh abstürzen, oft abstürzen"
Bryan Chen
16
Dafür gibt es buchstäblich Ausnahmen.
Raptortech97
Beachten Sie, dass Sie mit Codierungsverträgen viele solche Fehler statisch erkennen können. Dies ist im Allgemeinen dem Auslösen einer Ausnahme zur Laufzeit überlegen. Die Unterstützung und Wirksamkeit von Codierungsverträgen variiert je nach Sprache.
Brian
3
Da Sie Illegal werfen, ist es überflüssig zu sagen „ Eingang Person“.
Kevin Cline

Antworten:

29

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.

Kilian Foth
quelle
Frühes Scheitern ist der richtige Weg, wenn ein Fehler oder eine Störung nicht behoben werden kann. Wenn Sie beispielsweise eine Datei kopieren müssen und der Quell- oder Zielpfad leer ist, sollten Sie sofort eine Ausnahme auslösen.
Michael Shopsin
Es ist auch bekannt als "Fail Fast"
keuleJ
8

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. IllegalArgumentExceptionist 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:

public class PersonArgumentException extends IllegalArgumentException {
    public MyException(String message) {
        super(message);
    }
}

Dann, wenn jemand eine sieht PersonArgumentExceptionEs 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 '

Jonathan Eunice
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.
Blrfl
Das Peppen einer Codebasis mit benutzerdefinierten Ausnahmen sollte vermieden werden, da dies zwar hilfreich sein kann, jedoch nicht für andere, die sich mit ihnen vertraut machen müssen. Wenn Sie feststellen, dass Sie eine allgemeine Ausnahme erweitern und keine Mitglieder oder Funktionen hinzufügen, benötigen Sie diese normalerweise nicht. Auch in deinem Beispiel PersonArgumentExceptionist das nicht so klar wie IllegalArgumentException. 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 a Personfür einen Anruf in einem ungültigen Zustand befindet (ähnlich wie InvalidOperationExceptionin C #).
Selali Adobor
Wenn Sie nicht aufpassen, können Sie zwar dieselbe Ausnahme von einer anderen Stelle in der Funktion auslösen, dies ist jedoch ein Fehler im Code und nicht in der Natur der üblichen Ausnahmen. Und hier kommen Debugging- und Stack-Traces ins Spiel. Wenn Sie versuchen, Ihre Ausnahmen zu "kennzeichnen", verwenden Sie die vorhandenen Funktionen, die die häufigsten Ausnahmen bieten, z. B. den Nachrichtenkonstruktor (genau dafür ist er da). Einige Ausnahmen stellen Konstruktoren sogar zusätzliche Parameter zur Verfügung, um den Namen des problematischen Arguments abzurufen. Sie können der gleichen Funktion jedoch eine wohlgeformte Nachricht bereitstellen.
Selali Adobor
Ich gebe zu, dass die bloße Umbenennung einer gemeinsamen Ausnahme in eine Handelsmarke mit im Grunde derselben Ausnahme ein schwaches Beispiel ist und sich selten lohnt. In der Praxis versuche ich, benutzerdefinierte Ausnahmen auf einer höheren semantischen Ebene auszulösen, die enger mit der Absicht des Moduls verbunden sind.
Jonathan Eunice
Ich habe in den Bereichen Numerik / HPC und OS / Middleware leistungssensitive Arbeit geleistet. Ein Leistungsanstieg von 13% ist keine Kleinigkeit. Ich könnte einige Schecks ausschalten, um es zu bekommen, auf die gleiche Weise könnte ein Militärkommandant 105% der nominalen Reaktorleistung verlangen. Ich habe jedoch festgestellt, dass "es auf diese Weise schneller läuft", was häufig als Grund angegeben wird, Überprüfungen, Sicherungen und andere Schutzmaßnahmen zu deaktivieren. Es handelt sich im Grunde genommen um einen Kompromiss zwischen Ausfallsicherheit und Sicherheit (in vielen Fällen einschließlich Datenintegrität), um zusätzliche Leistung zu erzielen. Ob sich das lohnt, ist ein Urteilsspruch.
Jonathan Eunice
3

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, NullReferenceExceptionwird 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 assertAnweisungen verwenden, die:

  1. Sind kürzer zu schreiben und zu lesen:

        assert p1 : "p1 is null";
        assert p2 : "p2 is null";
    
  2. 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:

    • Behauptungen beziehen sich auf Programmierfehler ("/ * das sollte niemals passieren * /"),
    • Ausnahmen sind für Eckfälle (außergewöhnliche, aber wahrscheinliche Situationen)

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.

Core-Dump
quelle
1

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

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.
}

fester, weil

  1. Der Fehler wird so schnell wie möglich abgefangen, dh wenn 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.
  2. Es verringert die Kopplung zwischen Funktionen: Es werden registerPerson()keine Annahmen darüber getroffen, welche anderen Funktionen das personArgument verwenden und wie sie es verwenden: Die Entscheidung, bei der nulles sich um einen Fehler handelt, wird lokal getroffen und implementiert.

Insbesondere wenn der Code ziemlich komplex ist, bevorzuge ich diesen zweiten Ansatz.

Giorgio
quelle
1
Die Angabe des Grundes ("Eingabe-Person ist null.") Hilft auch, das Problem zu verstehen, anders als wenn eine undurchsichtige Methode in einer Bibliothek fehlschlägt.
Florian F
1

Im Allgemeinen ist es eine gute Idee, "früh zu scheitern". In Ihrem speziellen Beispiel bietet der explizite IllegalArgumentExceptionCode jedoch keine signifikante Verbesserung gegenüber a NullReferenceException-, da beide Objekte, mit denen gearbeitet wird, bereits als Argumente an die Funktion übergeben werden.

Aber schauen wir uns ein etwas anderes Beispiel an.

class PersonCalculator {
    PersonCalculator(Person p) {
        if (p == null) throw new ArgumentNullException("p");
        _p = p;
    }

    void Calculate() {
        // Do something to p
    }
}

Wenn der Konstruktor kein Argument enthält, wird NullReferenceExceptionbeim Aufruf ein angezeigt Calculate.

Aber das kaputte Stück Code war weder die CalculateFunktion noch der Konsument der CalculateFunktion. Das kaputte Stück Code war der Code, der versucht, das PersonCalculatormit einer Null zu konstruieren Person- also möchten wir, dass die Ausnahme auftritt.

Wenn wir diese explizite Argumentprüfung entfernen, müssen Sie herausfinden, warum ein NullReferenceExceptionbeim CalculateAufruf von aufgetreten ist . Das Auffinden, warum das Objekt mit einer nullPerson erstellt wurde, kann schwierig werden, insbesondere wenn der Code, der den Taschenrechner erstellt, nicht in der Nähe des Codes liegt, der die CalculateFunktion tatsächlich aufruft .

Pete
quelle
0

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.

Telastyn
quelle
-3

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.

Whatsisname
quelle
Ich arbeite seit ein paar Jahren mit einem Code, der auf diesem Prinzip basiert: Zeiger werden bei Bedarf einfach de-referenziert und fast nie gegen NULL geprüft und explizit behandelt. Das Debuggen dieses Codes war immer sehr zeitaufwändig und mühsam, da Ausnahmen (oder Core-Dumps) viel später auftreten, wenn der logische Fehler vorliegt. Ich habe buchstäblich Wochen damit verbracht, nach bestimmten Fehlern zu suchen. Aus diesem Grund bevorzuge ich den Fail-as-soon-as-possible-Stil, zumindest für komplexen Code, bei dem nicht sofort ersichtlich ist, woher eine Nullzeiger-Ausnahme stammt.
Giorgio
"Zum einen macht es keinen Sinn, dort sowieso eine Null zu übergeben, deshalb werde ich das Problem sofort anhand des Auslösens einer NullReferenceException herausfinden.": Nicht unbedingt, wie das zweite Beispiel von Prog zeigt.
Giorgio
"Zweitens, wenn jede Funktion leicht unterschiedliche Ausnahmen auslöst, basierend auf der Art der offensichtlich falschen Eingaben, dann haben Sie ziemlich bald Funktionen, die möglicherweise 18 verschiedene Arten von Ausnahmen auslösen ...": In diesem Fall Sie kann dieselbe Ausnahme (sogar NullPointerException) in allen Funktionen verwenden: Wichtig ist, dass Sie frühzeitig und nicht für jede Funktion eine andere Ausnahme auslösen.
Giorgio
Dafür gibt es RuntimeExceptions. Jede Bedingung, die "nicht passieren sollte", aber verhindert, dass Ihr Code funktioniert, sollte eine auslösen. Sie müssen sie nicht in der Methodensignatur deklarieren. Sie müssen sie jedoch irgendwann abfangen und melden, dass die Funktion der angeforderten Aktion fehlgeschlagen ist.
Florian F
1
Die Frage spezifiziert keine Sprache, daher ist das Verlassen auf zB RuntimeExceptionnicht unbedingt eine gültige Annahme.