Debug.Assert vs Exception Throwing

92

Ich habe viele Artikel (und einige ähnliche Fragen, die auf StackOverflow veröffentlicht wurden) darüber gelesen, wie und wann Behauptungen verwendet werden sollen, und ich habe sie gut verstanden. Trotzdem verstehe ich nicht, welche Art von Motivation mich antreiben sollte, Debug.Assertanstatt eine einfache Ausnahme zu machen. Was ich damit meine, ist, dass in .NET die Standardantwort auf eine fehlgeschlagene Behauptung darin besteht, "die Welt anzuhalten" und dem Benutzer ein Meldungsfeld anzuzeigen. Obwohl diese Art von Verhalten geändert werden könnte, finde ich es sehr ärgerlich und überflüssig, dies zu tun, während ich stattdessen einfach eine geeignete Ausnahme auslösen könnte. Auf diese Weise könnte ich den Fehler leicht in das Protokoll der Anwendung schreiben, bevor ich die Ausnahme auslöse, und außerdem friert meine Anwendung nicht unbedingt ein.

Warum sollte ich also, wenn überhaupt, Debug.Assertanstelle einer einfachen Ausnahme verwenden? Wenn Sie eine Behauptung dort platzieren, wo sie nicht sein sollte, kann dies zu allen Arten von "unerwünschtem Verhalten" führen. Aus meiner Sicht erhalte ich also wirklich nichts, wenn ich eine Behauptung verwende, anstatt eine Ausnahme auszulösen. Stimmen Sie mir zu oder fehlt mir hier etwas?

Hinweis: Ich verstehe voll und ganz, was der Unterschied "in der Theorie" ist (Debug vs. Release, Verwendungsmuster usw.), aber aus meiner Sicht ist es besser, eine Ausnahme auszulösen, als eine Bestätigung durchzuführen. Wenn ein Fehler in einer Produktionsversion entdeckt wird, möchte ich trotzdem, dass die "Behauptung" fehlschlägt (schließlich ist der "Overhead" lächerlich gering), daher ist es besser, stattdessen eine Ausnahme auszulösen.


Bearbeiten: So wie ich es sehe, bedeutet eine fehlgeschlagene Bestätigung, dass die Anwendung in einen beschädigten, unerwarteten Zustand übergegangen ist. Warum sollte ich die Ausführung fortsetzen wollen? Es spielt keine Rolle, ob die Anwendung auf einer Debug- oder Release-Version ausgeführt wird. Das gleiche gilt für beide

Gemeinschaft
quelle
1
Für Dinge, die Sie sagen "Wenn ein Fehler in einer Produktionsversion entdeckt wird, möchte ich immer noch, dass die" Behauptung "fehlschlägt", sollten Sie Ausnahmen verwenden
Tom Neyland
1
Leistung ist der einzige Grund. Wenn Sie die ganze Zeit nichts überprüfen, kann dies die Geschwindigkeit verringern, obwohl dies möglicherweise völlig unbemerkt bleibt. Dies gilt hauptsächlich für Fälle, die niemals auftreten sollten, z. B. wenn Sie wissen, dass Sie sie bereits in einer vorherigen Funktion auf Null überprüft haben. Es macht keinen Sinn, Zyklen zu verschwenden, um sie erneut zu überprüfen. Der debug.assert verhält sich effektiv wie ein Unit-Test der letzten Chance, um Sie zu informieren.
rollt

Antworten:

175

Obwohl ich der Meinung bin, dass Ihre Argumentation plausibel ist - das heißt, wenn eine Behauptung unerwartet verletzt wird, ist es sinnvoll, die Ausführung durch Werfen zu stoppen -, würde ich persönlich keine Ausnahmen anstelle von Behauptungen verwenden. Hier ist der Grund:

Wie andere gesagt haben, sollten Behauptungen Situationen dokumentieren , die unmöglich sind , so dass der Entwickler informiert wird, wenn die angeblich unmögliche Situation eintritt. Ausnahmen bieten dagegen einen Kontrollflussmechanismus für außergewöhnliche, unwahrscheinliche oder fehlerhafte Situationen, aber nicht für unmögliche Situationen. Für mich ist der Hauptunterschied folgender:

  • Es sollte IMMER möglich sein, einen Testfall zu erstellen, der eine bestimmte Wurfanweisung ausführt. Wenn es nicht möglich ist, einen solchen Testfall zu erstellen, haben Sie einen Codepfad in Ihrem Programm, der niemals ausgeführt wird, und dieser sollte als toter Code entfernt werden.

  • Es sollte NIEMALS möglich sein, einen Testfall zu erstellen, bei dem eine Behauptung ausgelöst wird. Wenn eine Zusicherung ausgelöst wird, ist entweder der Code falsch oder die Zusicherung ist falsch. In jedem Fall muss sich etwas im Code ändern.

Deshalb würde ich eine Behauptung nicht durch eine Ausnahme ersetzen. Wenn die Zusicherung nicht tatsächlich ausgelöst werden kann, bedeutet das Ersetzen durch eine Ausnahme, dass Sie einen nicht testbaren Codepfad in Ihrem Programm haben . Ich mag nicht testbare Codepfade nicht.

Eric Lippert
quelle
16
Das Problem mit Behauptungen ist, dass sie im Produktions-Build nicht vorhanden sind. Wenn eine angenommene Bedingung nicht erfüllt ist, hat Ihr Programm ein undefiniertes Verhaltensland betreten. In diesem Fall muss ein verantwortliches Programm die Ausführung so schnell wie möglich anhalten (das Abwickeln des Stapels ist auch etwas gefährlich, je nachdem, wie streng Sie werden möchten). Ja, Behauptungen sollten normalerweise nicht zu feuern sein, aber Sie wissen nicht, was möglich ist, wenn Dinge in freier Wildbahn ausgehen. Was Sie für unmöglich gehalten haben , kann in der Produktion passieren, und ein verantwortungsbewusstes Programm sollte verletzte Annahmen erkennen und umgehend handeln.
kizzx2
2
@ kizzx2: OK, wie viele unmögliche Ausnahmen pro Zeile Produktionscode schreiben Sie?
Eric Lippert
4
@ kixxx2: Dies ist C #, so dass Sie können Behauptungen in der Produktion Code unter Verwendung Trace.Assert zu halten. Sie können sogar die Datei app.config verwenden, um Produktionszusicherungen in eine Textdatei umzuleiten, anstatt unhöflich gegenüber dem Endbenutzer zu sein.
HTTP 410
3
@AnorZaken: Ihr Punkt zeigt einen Konstruktionsfehler in Ausnahmen. Wie ich bereits an anderer Stelle erwähnt habe, sind Ausnahmen (1) tödliche Katastrophen, (2) Fehler ohne Kopf, die niemals auftreten sollten, (3) Konstruktionsfehler, bei denen eine Ausnahme verwendet wird, um einen nicht außergewöhnlichen Zustand anzuzeigen, oder (4) unerwartete exogene Zustände . Warum werden diese vier völlig unterschiedlichen Dinge alle durch Ausnahmen dargestellt? Wenn ich meine druthers hatte, boneheaded „null wurde dereferenziert“ Ausnahmen würden nicht abfangbar sein überhaupt . Es ist nie richtig und sollte Ihr Programm beenden, bevor es mehr Schaden anrichtet . Sie sollten eher wie Behauptungen sein.
Eric Lippert
2
@Backwards_Dave: Falsche Behauptungen sind schlecht, keine wahren. Mithilfe von Zusicherungen können Sie teure Überprüfungen durchführen, die Sie in der Produktion nicht ausführen möchten. Und wenn eine Behauptung in der Produktion verletzt wird, was sollte der Endbenutzer dagegen tun?
Eric Lippert
17

Behauptungen werden verwendet, um das Verständnis des Programmierers für die Welt zu überprüfen. Eine Zusicherung sollte nur fehlschlagen, wenn der Programmierer etwas falsch gemacht hat. Verwenden Sie beispielsweise niemals eine Zusicherung, um Benutzereingaben zu überprüfen.

Aktiviert den Test für Bedingungen, die "nicht auftreten können". Ausnahmen sind Bedingungen, die "nicht passieren sollten, aber tun".

Behauptungen sind nützlich, da Sie zur Erstellungszeit (oder sogar zur Laufzeit) ihr Verhalten ändern können. Beispielsweise werden in Release-Builds die Asserts häufig nicht einmal überprüft, da sie unnötigen Overhead verursachen. Dies ist auch etwas zu beachten: Ihre Tests werden möglicherweise nicht einmal ausgeführt.

Wenn Sie Ausnahmen anstelle von Asserts verwenden, verlieren Sie etwas an Wert:

  1. Der Code ist ausführlicher, da das Testen und Auslösen einer Ausnahme mindestens zwei Zeilen umfasst, während eine Zusicherung nur eine ist.

  2. Ihr Test- und Wurfcode wird immer ausgeführt, während Asserts kompiliert werden können.

  3. Sie verlieren die Kommunikation mit anderen Entwicklern, da Asserts eine andere Bedeutung haben als der Produktcode, der überprüft und ausgelöst wird. Wenn Sie eine Programmierzusicherung wirklich testen, verwenden Sie eine Bestätigung.

Mehr hier: http://nedbatchelder.com/text/assert.html

Ned Batchelder
quelle
Wenn es "nicht passieren kann", warum dann eine Behauptung schreiben? Ist das nicht überflüssig? Wenn es tatsächlich passieren kann, aber nicht sollte, ist dies dann nicht dasselbe wie "sollte nicht passieren, aber tun", was für Ausnahmen gilt?
David Klempfner
2
"Kann nicht passieren" steht aus einem Grund in Anführungszeichen: Es kann nur passieren, wenn der Programmierer in einem anderen Teil des Programms etwas falsch gemacht hat. Die Behauptung ist eine Überprüfung gegen Programmiererfehler.
Ned Batchelder
@NedBatchelder Der Begriff Programmierer ist jedoch etwas mehrdeutig, wenn Sie eine Bibliothek entwickeln. Ist es richtig, dass diese "unmöglichen Situationen" für den Bibliotheksbenutzer unmöglich sein sollten , aber möglich sein könnten, wenn der Bibliotheksautor einen Fehler gemacht hat?
Bruno Zell
12

BEARBEITEN: Als Antwort auf die Bearbeitung / Notiz, die Sie in Ihrem Beitrag vorgenommen haben: Es klingt so, als ob die Verwendung von Ausnahmen das Richtige ist, anstatt Behauptungen für die Art von Dingen zu verwenden, die Sie ausführen möchten. Ich denke, der mentale Stolperstein, auf den Sie stoßen, besteht darin, dass Sie Ausnahmen und Behauptungen in Betracht ziehen, um denselben Zweck zu erfüllen, und Sie versuchen herauszufinden, welche davon „richtig“ wäre. Es gibt zwar einige Überschneidungen bei der Verwendung von Behauptungen und Ausnahmen, aber verwechseln Sie nicht, dass sie unterschiedliche Lösungen für dasselbe Problem darstellen - sie sind es nicht. Behauptungen und Ausnahmen haben jeweils ihren eigenen Zweck, ihre eigenen Stärken und Schwächen.

Ich wollte eine Antwort in meinen eigenen Worten schreiben, aber dies wird dem Konzept besser gerecht als ich es hätte:

C # Station: Behauptungen

Die Verwendung von assert-Anweisungen kann ein effektiver Weg sein, um Programmlogikfehler zur Laufzeit abzufangen, und dennoch können sie leicht aus dem Produktionscode herausgefiltert werden. Sobald die Entwicklung abgeschlossen ist, können die Laufzeitkosten dieser redundanten Tests für Codierungsfehler einfach durch Definieren des Präprozessorsymbols NDEBUG [das alle Zusicherungen deaktiviert] während der Kompilierung eliminiert werden. Denken Sie jedoch daran, dass der in der Zusicherung selbst platzierte Code in der Produktionsversion weggelassen wird.

Eine Behauptung wird am besten verwendet, um eine Bedingung nur dann zu testen, wenn alle der folgenden Bedingungen erfüllt sind:

* the condition should never be false if the code is correct,
* the condition is not so trivial so as to obviously be always true, and
* the condition is in some sense internal to a body of software.

Behauptungen sollten fast nie verwendet werden, um Situationen zu erkennen, die während des normalen Betriebs der Software auftreten. Beispielsweise sollten Assertions normalerweise nicht verwendet werden, um die Eingabe eines Benutzers auf Fehler zu überprüfen. Es kann jedoch sinnvoll sein, Assertions zu verwenden, um zu überprüfen, ob ein Anrufer die Eingaben eines Benutzers bereits überprüft hat.

Verwenden Sie grundsätzlich Ausnahmen für Dinge, die in einer Produktionsanwendung abgefangen / behandelt werden müssen, und verwenden Sie Assertions, um logische Überprüfungen durchzuführen, die für die Entwicklung nützlich sind, aber in der Produktion deaktiviert sind.

Tom Neyland
quelle
Ich erkenne das alles. Aber die Sache ist, dass dieselbe Aussage, die Sie als fett markiert haben, auch für Ausnahmen gilt. So wie ich es sehe, könnte ich anstelle einer Behauptung einfach eine Ausnahme auslösen (da die "Situation, die niemals eintreten sollte" bei einer bereitgestellten Version auftritt, möchte ich trotzdem darüber informiert werden [plus die Anwendung kann in einen beschädigten Zustand eintreten, daher ist eine Ausnahme geeignet, ich möchte möglicherweise nicht den normalen Ausführungsfluss fortsetzen)
1
Behauptungen sollten für Invarianten verwendet werden. Ausnahmen sollten verwendet werden, wenn beispielsweise etwas nicht null sein sollte, sondern (wie ein Parameter für eine Methode).
Ed S.
Ich denke, alles hängt davon ab, wie defensiv Sie codieren möchten.
Ned Batchelder
Ich stimme zu, für das, was Sie anscheinend brauchen, sind Ausnahmen der richtige Weg. Sie sagten, Sie möchten: In der Produktion festgestellte Fehler, die Fähigkeit, Informationen über Fehler zu protokollieren, und die Kontrolle des Ausführungsflusses usw. Diese drei Dinge lassen mich denken, dass Sie nur einige Ausnahmen umgehen müssen.
Tom Neyland
7

Ich denke, ein (erfundenes) praktisches Beispiel kann helfen, den Unterschied zu beleuchten:

(angepasst aus der Batch-Erweiterung von MoreLinq )

// 'public facing' method
public int DoSomething(List<string> stuff, object doohickey, int limit) {

    // validate user input and report problems externally with exceptions

    if(stuff == null) throw new ArgumentNullException("stuff");
    if(doohickey == null) throw new ArgumentNullException("doohickey");
    if(limit <= 0) throw new ArgumentOutOfRangeException("limit", limit, "Should be > 0");

    return DoSomethingImpl(stuff, doohickey, limit);
}

// 'developer only' method
private static int DoSomethingImpl(List<string> stuff, object doohickey, int limit) {

    // validate input that should only come from other programming methods
    // which we have control over (e.g. we already validated user input in
    // the calling method above), so anything using this method shouldn't
    // need to report problems externally, and compilation mode can remove
    // this "unnecessary" check from production

    Debug.Assert(stuff != null);
    Debug.Assert(doohickey != null);
    Debug.Assert(limit > 0);

    /* now do the actual work... */
}

Wie Eric Lippert et al. Gesagt haben, behaupten Sie nur Dinge, von denen Sie erwarten, dass sie korrekt sind, nur für den Fall, dass Sie (der Entwickler) sie versehentlich an einer anderen Stelle falsch verwendet haben , damit Sie Ihren Code reparieren können. Sie lösen grundsätzlich Ausnahmen aus, wenn Sie keine Kontrolle darüber haben oder nicht vorhersehen können, was hereinkommt, z. B. für Benutzereingaben , damit die fehlerhaften Daten angemessen reagieren können (z. B. der Benutzer).

drzaus
quelle
Sind Ihre 3 Asserts nicht vollständig überflüssig? Es ist unmöglich, dass ihre Parameter als falsch bewertet werden.
David Klempfner
1
Das ist der Punkt - die Behauptungen sind dazu da, Dinge zu dokumentieren, die unmöglich sind. Warum würdest du das tun? Weil Sie möglicherweise so etwas wie ReSharper haben, das Sie innerhalb der DoSomethingImpl-Methode warnt, dass "Sie hier möglicherweise null dereferenzieren" und Sie sagen möchten: "Ich weiß, was ich tue, dies kann niemals null sein". Dies ist auch ein Hinweis für einige spätere Programmierer, die die Verbindung zwischen DoSomething und DoSomethingImpl möglicherweise nicht sofort erkennen, insbesondere wenn sie Hunderte von Zeilen voneinander entfernt sind.
Marcel Popescu
4

Ein weiteres Nugget von Code Complete :

"Eine Behauptung ist eine Funktion oder ein Makro, das sich lautstark beschwert, wenn eine Annahme nicht wahr ist. Verwenden Sie Behauptungen, um im Code gemachte Annahmen zu dokumentieren und unerwartete Bedingungen auszuspülen. ...

"Während der Entwicklung werden durch Behauptungen widersprüchliche Annahmen, unerwartete Bedingungen, schlechte Werte, die an Routinen übergeben wurden, usw. beseitigt."

Er fügt einige Richtlinien hinzu, was behauptet werden soll und was nicht.

Ausnahmen dagegen:

"Verwenden Sie die Ausnahmebehandlung, um auf unerwartete Fälle aufmerksam zu machen. Ausnahmefälle sollten so behandelt werden, dass sie während der Entwicklung offensichtlich sind und wiederhergestellt werden können, wenn Produktionscode ausgeführt wird."

Wenn Sie dieses Buch nicht haben, sollten Sie es kaufen.

Andrew Cowenhoven
quelle
2
Ich habe das Buch gelesen, es ist ausgezeichnet. Allerdings .. du hast meine Frage nicht beantwortet :)
Du hast recht, ich habe es nicht beantwortet. Meine Antworten sind nein, ich stimme Ihnen nicht zu. Behauptungen und Ausnahmen sind verschiedene Tiere, wie oben beschrieben, und einige der anderen hier veröffentlichten Antworten.
Andrew Cowenhoven
0

Debug.Assert funktioniert standardmäßig nur in Debug-Builds. Wenn Sie also in Ihren Release-Builds ein unerwartetes Verhalten feststellen möchten, müssen Sie Ausnahmen verwenden oder die Debug-Konstante in Ihren Projekteigenschaften aktivieren (was in berücksichtigt wird) allgemein keine gute Idee zu sein).

Mez
quelle
Der erste Teilsatz ist wahr, der Rest ist im Allgemeinen eine schlechte Idee: Behauptungen sind Annahmen und keine Validierung (wie oben angegeben), das Aktivieren des Debuggens in der Version ist wirklich keine Option.
Marc Wittke
0

Verwenden Behauptungen für Dinge , die ARE möglich, sollte aber nicht passieren (wenn es nicht möglich wäre, warum würden Sie eine Behauptung sagen?).

Klingt das nicht nach einem Fall, in dem man einen verwendet Exception? Warum würden Sie eine Behauptung anstelle einer verwenden Exception?

Weil es Code geben sollte, der vor Ihrer Zusicherung aufgerufen wird, der verhindern würde, dass der Parameter der Zusicherung falsch ist.

Normalerweise gibt es keinen Code vor Ihnen Exception, der garantiert, dass er nicht geworfen wird.

Warum ist es gut, dass Debug.Assert()in prod zusammengestellt wird? Wenn Sie es im Debug wissen möchten, möchten Sie es nicht im Produkt wissen?

Sie möchten es nur während der Entwicklung, denn sobald Sie Debug.Assert(false)Situationen gefunden haben, schreiben Sie Code, um sicherzustellen, dass Debug.Assert(false)dies nicht erneut geschieht. Sobald die Entwicklung abgeschlossen ist, wird davon ausgegangen, dass Sie die Debug.Assert(false)Situationen gefunden und behoben habenDebug.Assert() können Sie sie sicher kompilieren, da sie jetzt redundant sind.

David Klempfner
quelle
0

Angenommen, Sie sind Mitglied eines ziemlich großen Teams und es gibt mehrere Personen, die alle an derselben allgemeinen Codebasis arbeiten, einschließlich der Überlappung von Klassen. Sie können eine Methode erstellen, die von mehreren anderen Methoden aufgerufen wird. Um Sperrenkonflikte zu vermeiden, fügen Sie ihr keine separate Sperre hinzu, sondern "nehmen an", dass sie zuvor von der aufrufenden Methode mit einer bestimmten Sperre gesperrt wurde. Zum Beispiel Debug.Assert (RepositoryLock.IsReadLockHeld || RepositoryLock.IsWriteLockHeld); Die anderen Entwickler übersehen möglicherweise einen Kommentar, der besagt, dass die aufrufende Methode die Sperre verwenden muss, können dies jedoch nicht ignorieren.

Daniel
quelle