Ich wollte eine solide Antwort auf die Frage finden, ob Laufzeitprüfungen durchgeführt werden sollen, um Eingaben zu validieren, um sicherzustellen, dass ein Kunde das vertraglich festgelegte Ende der Vereinbarung eingehalten hat. Stellen Sie sich zum Beispiel einen einfachen Klassenkonstruktor vor:
class Foo
{
public:
Foo( BarHandle bar )
{
FooHandle handle = GetFooHandle( bar );
if( handle == NULL ) {
throw std::exception( "invalid FooHandle" );
}
}
};
Ich würde in diesem Fall argumentieren, dass ein Benutzer nicht versuchen sollte, eine Foo
ohne eine gültige zu erstellen BarHandle
. Es scheint nicht richtig zu sein, zu überprüfen, ob dies bar
innerhalb des Foo
Konstruktors gültig ist . Wenn ich einfach dokumentiere, dass Foo
der Konstruktor eine gültige erfordert BarHandle
, ist das nicht genug? Ist dies ein angemessener Weg, um meine Vertragsvoraussetzungen vertraglich durchzusetzen?
Bisher hat alles, was ich gelesen habe, gemischte Meinungen dazu. Es scheint, als würden 50% der Leute sagen, dass sie überprüfen sollen, ob sie bar
gültig sind, die anderen 50% würden sagen, dass ich es nicht tun sollte. Betrachten Sie beispielsweise einen Fall, in dem der Benutzer überprüft, ob sie BarHandle
korrekt sind, aber eine zweite (und unnötige) Überprüfung wird auch innerhalb des Foo
Konstruktors ausgeführt.
quelle
Antworten:
Ich glaube nicht, dass es eine einzige Antwort darauf gibt. Ich denke, die Hauptsache, die notwendig ist, ist Konsistenz - entweder erzwingen Sie alle Voraussetzungen für eine Funktion, oder Sie versuchen nicht, eine davon durchzusetzen . Leider ist das ziemlich selten - was normalerweise passiert, ist, dass Programmierer, anstatt über die Voraussetzungen nachzudenken und sie durchzusetzen, Codebits hinzufügen, um Vorbedingungen durchzusetzen, deren Verletzung beim Testen zu Fehlern geführt hat, aber häufig andere Möglichkeiten offen lassen, die jedoch zu Fehlern führen können kam beim Testen nicht vor.
In vielen Fällen ist es durchaus sinnvoll, zwei Ebenen bereitzustellen: eine für die "interne" Verwendung, bei der keine Vorbedingungen erzwungen werden, und eine zweite für die "externe" Verwendung, bei der nur die Voraussetzungen erzwungen werden, und die erste.
Ich denke jedoch, dass es besser ist, die Voraussetzungen im Quellknoten durchzusetzen und nicht nur zu dokumentieren. Eine Ausnahme oder Behauptung ist viel schwieriger zu ignorieren als die Dokumentation und bleibt mit größerer Wahrscheinlichkeit mit dem Rest des Codes synchron.
quelle
foo
NULL ist, aber NULL zu sein ist nicht der einzige Wegfoo
, der ungültig sein könnte. Was ist zum Beispiel mit -1, das in a umgewandelt wirdFooHandle
? Ich kann nicht alle möglichen Möglichkeiten überprüfen, wie das Handle ungültig sein könnte. NULL ist eine offensichtliche Wahl und etwas, auf das normalerweise geprüft wird, aber keine abschließende Prüfung. Was würden Sie hier empfehlen?Es ist eine sehr schwierige Frage, da es verschiedene Konzepte gibt:
Dies ist jedoch meist ein Artefakt einer Art Fehler, in diesem Fall. Die Nullheit wird besser durch Typeinschränkungen erzwungen, da der Compiler diese tatsächlich überprüft. Da jedoch nicht alles in einem Typsystem erfasst werden kann, insbesondere in C ++, lohnt sich die Frage selbst immer noch.
Persönlich denke ich, dass Korrektheit und Dokumentation von größter Bedeutung sind. Schnell und falsch zu sein ist nutzlos. Schnell und nur manchmal falsch zu sein ist ein bisschen besser, bringt aber auch nicht viel auf den Tisch.
Die Leistung kann jedoch in einigen Teilen der Programme kritisch sein, und einige Überprüfungen können sehr umfangreich sein (dh beweisen, dass auf einen gerichteten Graphen alle Knoten sowohl zugänglich als auch gemeinsam zugänglich sind). Daher würde ich für einen doppelten Ansatz stimmen.
Prinzip eins: Schnell scheitern . Dies ist ein Leitprinzip in der defensiven Programmierung im Allgemeinen, das die frühestmögliche Erkennung von Fehlern befürwortet. Ich würde Fail Hard zur Gleichung hinzufügen .
Leider ist ein hartes Versagen in einer Produktionsumgebung nicht unbedingt die beste Lösung. In diesem Fall kann eine bestimmte Ausnahme dazu beitragen, dass Sie schnell von dort wegkommen und einen hochrangigen Handler den fehlgeschlagenen Fall angemessen erfassen und behandeln können (höchstwahrscheinlich wird ein neuer Fall protokolliert und vorangetrieben).
Dies spricht jedoch nicht das Problem teurer Tests an. An Hot Spots können diese Tests zu viel kosten. In diesem Fall ist es sinnvoll, den Test nur in DEBUG-Builds zu aktivieren.
Dies lässt uns eine schöne und einfache Lösung:
SOFT_ASSERT(Cond_, Text_)
DEBUG_ASSERT(Cond_, Text_)
Wo die beiden Makros folgendermaßen definiert sind:
quelle
Ein Zitat, das ich dazu gehört habe, ist:
"Seien Sie konservativ in dem, was Sie tun, und liberal in dem, was Sie akzeptieren."
Was darauf hinausläuft, den Verträgen für Argumente zu folgen, wenn Sie Funktionen aufrufen, und alle Eingaben zu überprüfen, bevor Sie beim Schreiben von Funktionen handeln.
Letztendlich kommt es auf die Domain an. Wenn Sie eine Betriebssystem-API ausführen, sollten Sie jede Eingabe überprüfen und nicht allen eingehenden Daten als gültig vertrauen, bevor Sie darauf reagieren. Wenn Sie eine Bibliothek für andere Benutzer erstellen, lassen Sie den Benutzer sich selbst verarschen (OpenGL fällt Ihnen aus einem unbekannten Grund zuerst ein).
BEARBEITEN: Im OO-Sinne scheint es zwei Ansätze zu geben - einen, der besagt, dass ein Objekt niemals für die gesamte Zeit, in der auf ein Objekt zugegriffen werden darf, fehlerhaft sein sollte (alle seine Invarianten müssen wahr sein), und einen anderen Ansatz, der besagt, dass Sie einen Konstruktor haben Wenn nicht alle Invarianten festgelegt sind, legen Sie einige weitere Werte fest und haben eine zweite Initialisierungsfunktion, die init beendet.
Ich mag das erstere irgendwie besser, da es kein magisches Wissen erfordert oder sich auf die aktuelle Dokumentation stützt, um zu wissen, welche Teile der Initialisierung der Konstruktor nicht tut.
quelle