Frage

Mögliches Duplikat:
Defensivprogrammierung

Wir hatten heute Morgen eine großartige Diskussion über das Thema Defensivprogrammierung. Wir hatten eine Codeüberprüfung, in der ein Zeiger übergeben wurde und nicht überprüft wurde, wenn er gültig war.

Einige Leute waren der Meinung, dass nur ein Scheck auf Nullzeiger benötigt wurde. Ich fragte mich, ob es auf einer höheren Ebene und nicht auf jeder Methode überprüft werden könnte, und die Überprüfung auf Null war eine sehr begrenzte Überprüfung, ob das Objekt am anderen Ende des Punktes bestimmte Anforderungen nicht entsprach.

Ich verstehe und stimme zu, dass ein Scheck über NULL besser als nichts ist, aber es ist mir das Gefühl, dass die Überprüfung nur nach NULL ein falsches Sicherheitsgefühl vermittelt, da er im Bereich der Umfang begrenzt ist. Wenn Sie sicherstellen möchten, dass der Zeiger nutzbar ist, überprüfen Sie mehr als den Null.

Was sind Ihre Erfahrungen zu diesem Thema? Wie schreiben Sie Verteidigungen in Ihren Code für Parameter, die an untergeordnete Methoden übergeben werden?

War es hilfreich?

Lösung

In Code Complete 2 wurde ich im Kapitel über Fehlerbehandlung in die Idee von Barrikaden eingeführt. Im Wesentlichen ist eine Barrikade Code, die alle Eingaben rigoros bestätigt. Der Code in der Barrikade kann davon ausgehen, dass bereits ungültige Eingaben behandelt wurden und dass die empfangenen Eingaben gut sind. In der Barrikade muss Code nur um ungültige Daten sorgen, die von einem anderen Code innerhalb der Barrikade übergeben wurden. Die Behauptung von Bedingungen und vernünftiger Beteilungstests kann Ihr Vertrauen in den verbarrikadierten Code erhöhen. Auf diese Weise programmieren Sie sehr defensiv auf der Barrikade, aber weniger in der Barrikade. Eine andere Möglichkeit, darüber nachzudenken, besteht darin, dass Sie bei der Barrikade immer korrekt mit Fehlern umgehen und in der Barrikade lediglich die Bedingungen in Ihrem Debug -Build behaupten.

Was die Verwendung von Rohzeigern angeht, ist normalerweise das Beste, was Sie tun können, dass der Zeiger nicht null ist. Wenn Sie wissen, was in dieser Erinnerung sein soll, können Sie sicherstellen, dass der Inhalt in irgendeiner Weise konsistent ist. Dies wirft die Frage auf, warum diese Erinnerung nicht in ein Objekt eingewickelt ist, das die Konsistenz selbst überprüfen kann.

Warum verwenden Sie in diesem Fall einen Rohzeiger? Wäre es besser, eine Referenz oder einen intelligenten Zeiger zu verwenden? Enthält der Zeiger numerische Daten, und wenn ja, wäre es besser, ihn in ein Objekt einzuwickeln, das den Lebenszyklus dieses Zeigers verwaltet?

Die Beantwortung dieser Fragen kann Ihnen helfen, einen Weg zu finden, um defensiver zu sein, da Sie ein Design haben, das leichter zu verteidigen ist.

Andere Tipps

Der beste Weg, um defensiv zu sein Vermeiden Sie es, Zeiger zu verwenden, die zunächst null sein können

Wenn das in das übergebene Objekt nicht null sein darf, verwenden Sie eine Referenz! Oder bestehen Sie es nach Wert! Oder verwenden Sie einen intelligenten Zeiger.

Der beste Weg, um defensive Programme durchzuführen, besteht darin, Ihre Fehler zur Kompilierungszeit zu erfassen. Wenn es als Fehler angesehen wird, dass ein Objekt null oder auf Müll verweist, sollten Sie diese Dinge erstellen lassen, die Fehler kompilieren.

Letztendlich haben Sie keine Möglichkeit zu wissen, ob ein Zeiger auf ein gültiges Objekt zeigt. Also anstatt nach zu überprüfen eines Spezifischer Eckfall (der weitaus seltener ist als die wirklich gefährlichen Zeiger, die auf ungültige Objekte hinweisen), machen den Fehler unmöglich, indem sie einen Datentyp verwenden, der die Gültigkeit garantiert.

Ich kann mir eine andere Mainstream-Sprache nicht vorstellen, mit der Sie bei der Kompilierungszeit so viele Fehler aufnehmen können wie C ++. Verwenden Sie diese Fähigkeit.

Es gibt keine Möglichkeit zu überprüfen, ob ein Zeiger gültig ist.

Insgesamt hängt es davon ab, wie viele Fehler Sie Ihnen zugefügt haben möchten.

Das Überprüfen auf einen Nullzeiger ist definitiv etwas, das ich für notwendig, aber nicht ausreichend halten würde. Es gibt viele andere solide Prinzipien, die Sie mit Einstiegspunkten Ihres Codes beginnen können (z. B. Eingabevalidierung = tut dieser Zeiger auf etwas Nützliches) und beenden Sie Punkte (z. B. Sie dachten, der Zeiger wies auf etwas Nützliches hin, aber es zufällig verursachte es zufällig Ihr Code, um eine Ausnahme zu machen).

Kurz gesagt, wenn Sie davon ausgehen, dass jeder, der Ihren Code anruft, sein Bestes geben wird, um Ihr Leben zu ruinieren, werden Sie wahrscheinlich viele der schlimmsten Schuldigen finden.

Bearbeiten für Klarheit: Einige andere Antworten sprechen von Unit -Tests. Ich bin fest davon überzeugt, dass der Testcode manchmal ist mehr wertvoll als der Code, den es testet (abhängig davon, wer den Wert misst). Trotzdem denke ich auch, dass Einheiten -Tests sind Auch notwendig, aber nicht ausreichend für die defensive Codierung.

Konkretes Beispiel: Betrachten Sie eine Suchmethode der Drittanbieter, die dokumentiert ist, um eine Sammlung von Werten zurückzugeben, die Ihrer Anfrage entsprechen. Was in der Dokumentation für diese Methode nicht klar war, ist, dass der ursprüngliche Entwickler entschied, dass es besser wäre, einen Null zurückzugeben als eine leere Sammlung, wenn nichts mit Ihrer Anfrage übereinstimmt.

Jetzt nennen Sie Ihre defensive und gut getestete Methode zum Nachdenken (das fehlt leider ein interner Null-Zeiger-Check) und Boom! NullPointerexception, dass Sie ohne interne Überprüfung keine Möglichkeit haben, mit:

defensiveMethod(thirdPartySearch("Nothing matches me")); 
// You just passed a null to your own code.

Ich bin ein großer Fan der Designschule "Let It Crash". (Haftungsausschluss: Ich arbeite nicht an medizinischen Geräten, Avionik oder Kernkraft.) Wenn Ihr Programm aufbläst, starten Sie den Debugger und finden heraus, warum. Wenn Ihr Programm im Gegensatz dazu läuft, nachdem illegale Parameter festgestellt wurden, werden Sie zum Zeitpunkt des Absturzes wahrscheinlich keine Ahnung haben, was schief gelaufen ist.

Guter Code besteht aus vielen kleinen Funktionen/Methoden, und es macht es schwieriger, zu lesen und zu warten, was ein Dutzend Codesausschnitte zu lesen und zu pflegen, ein Dutzend Zeilen der Parameterprüfung erschwert. Halte es einfach.

Ich mag ein bisschen extrem sein, aber ich mag keine defensive Programmierung, ich denke, es ist Faulheit, die das Prinzip eingeführt hat.

Für dieses spezielle Beispiel ist in der Behauptung, dass der Zeiger nicht null ist, keinen Sinn. Wenn Sie einen Nullzeiger wünschen, gibt es keinen besseren Weg, ihn tatsächlich durchzusetzen (und ihn gleichzeitig klar zu dokumentieren), als stattdessen eine Referenz zu verwenden. Und es ist eine Dokumentation, die tatsächlich vom Compiler durchgesetzt wird und zur Laufzeit keinen Ziltch kostet !!

Im Allgemeinen neige ich dazu, keine "Roh" -Typen direkt zu verwenden. Lassen Sie uns veranschaulichen:

void myFunction(std::string const& foo, std::string const& bar);

Was sind die möglichen Werte von foo und bar ? Nun, das ist nur durch was a begrenzt std::string Kann enthalten ... was ziemlich vage ist.

Auf der anderen Seite:

void myFunction(Foo const& foo, Bar const& bar);

ist viel besser!

  • Wenn Menschen die Reihenfolge der Argumente fälschlicherweise umkehren, wird der Compiler erkannt
  • Jede Klasse ist ausschließlich für die Überprüfung verantwortlich, ob der Wert richtig ist und die Benutzer nicht belastet sind.

Ich neige dazu, eine starke Typisierung zu bevorzugen. Wenn ich einen Eintrag habe, der nur aus alphabetischen Zeichen bestehen und bis zu 12 Zeichen sein sollte, würde ich lieber eine kleine Klasse erstellen, um a zu wickeln std::string, mit einem einfachen validate Die Methode, die intern verwendet wird, um die Zuordnungen zu überprüfen und diese Klasse stattdessen zu übergeben. Auf diese Weise weiß ich, dass ich, wenn ich die Validierungsroutine einmal teste, mir keine Sorgen um alle Pfade machen muss, auf denen dieser Wert zu mir gelangen kann.

Das ist mir natürlich nicht, dass der Code nicht getestet werden sollte. Es ist nur so, dass ich eine starke Kapselung bevorzuge, und meiner Meinung nach ist die Validierung eines Eingangs Teil der Wissenskapselung.

Und da keine Regel ausnahmslos kommen kann ... wird die exponierte Schnittstelle notwendigerweise mit Validierungscode aufgebläht, da Sie nie wissen, was auf Sie zukommen könnte. Bei selbst validierenden Objekten in Ihrer Bom ist es jedoch im Allgemeinen ziemlich transparent.

"Unit -Tests, die den Code überprüfen, macht das, was er tun sollte"> "Produktionscode, der versucht, nicht das zu tun, was er nicht tun soll".

Ich würde nicht einmal auf Null selbst suchen, es sei denn, sie ist Teil einer veröffentlichten API.

Es kommt sehr darauf an; Wird die betreffende Methode jemals von Code extern in Ihrer Gruppe aufgerufen, oder ist es eine interne Methode?

Bei internen Methoden können Sie genug testen, um dies zu einem Streitpunkt zu machen. Wenn Sie Code erstellen, bei dem das Ziel die höchstmögliche Leistung ist, möchten Sie möglicherweise nicht die Zeit für die Überprüfung der Eingaben verbringen. Sie sind verdammt sicher, dass sie richtig sind.

Für extern sichtbare Methoden - wenn Sie welche haben - sollten Sie Ihre Eingaben immer überprüfen. Stets.

Aus Sicht der Debugging ist es am wichtigsten, dass Ihr Code fehlgeschlagen ist. Je früher der Code fehlschlägt, desto leichter ist es, den Ausfallpunkt zu finden.

Bei internen Methoden halten wir uns normalerweise an Behauptungen für diese Art von Schecks. Damit werden Fehler in Unit -Tests (Sie haben eine gute Testabdeckung, oder?) Oder zumindest in Integrationstests, die mit Behauptungen ausgeführt werden.

Überprüfen Sie nach Nullzeiger ist Nur die Hälfte der Geschichte, du solltest auch Weisen Sie jedem nicht zugewiesenen Zeiger einen Nullwert zu.
Die verantwortungsbewussteste API wird dasselbe tun.
Wenn Sie nach einem Null -Zeiger in CPU -Zyklen sehr günstig sind, können Sie und Ihr Unternehmen eine Anwendung zum Absturz seines Sturzes für Geld und Ruf kosten.

Sie können Null -Zeigerüberprüfungen überspringen, wenn sich der Code in einer privaten Schnittstelle befindet, über die Sie die vollständige Kontrolle haben und/oder Sie nach Null suchen, indem Sie einen Unit -Test oder einen Debug -Build -Test ausführen (z. B. Assert).

Hier in dieser Frage arbeiten ein paar Dinge bei der Arbeit, die ich ansprechen möchte:

  1. Die Codierungsrichtlinien sollten festlegen, dass Sie entweder mit einer Referenz oder einem Wert direkt zu tun haben, anstatt Zeiger zu verwenden. Per Definition sind Zeiger Werttypen, die nur eine Adresse im Speicher enthalten - die Gültigkeit eines Zeigers ist plattformspezifisch und bedeutet viele Dinge (Bereich des adressierbaren Speichers, Plattform usw.).
  2. Wenn Sie jemals einen Zeiger aus irgendeinem Grund benötigen (wie für dynamisch erzeugte und polymorphe Objekte), sollten Sie intelligente Zeiger verwenden. Intelligente Zeiger bieten Ihnen viele Vorteile mit der Semantik "normaler" Zeiger.
  3. Wenn ein Typ zum Beispiel einen "ungültigen" Status hat, sollte der Typ selbst dafür bereitstellen. Insbesondere können Sie das Nullobject-Muster implementieren, das angibt, wie sich ein "schlecht definiertes" oder "nicht initialisiertes" Objekt verhält (möglicherweise durch Ausnahmen oder durch Bereitstellung von Mitgliedfunktionen).

Sie können einen intelligenten Zeiger erstellen, der die NullObject -Standardeinstellung aussieht, die so aussieht:

template <class Type, class NullTypeDefault>
struct possibly_null_ptr {
  possibly_null_ptr() : p(new NullTypeDefault) {}
  possibly_null_ptr(Type* p_) : p(p_) {}
  Type * operator->() { return p.get(); }
  ~possibly_null_ptr() {}
  private:
    shared_ptr<Type> p;
    friend template<class T, class N> Type & operator*(possibly_null_ptr<T,N>&);
};

template <class Type, class NullTypeDefault>
Type & operator*(possibly_null_ptr<Type,NullTypeDefault> & p) {
  return *p.p;
}

Dann benutze das possibly_null_ptr<> Vorlage in Fällen, in denen Sie möglicherweise Null -Zeiger auf Typen unterstützen, die ein Standard -"Nullverhalten" haben. Dies macht es im Entwurf explizit, dass es ein akzeptables Verhalten für "Null -Objekte" gibt, und dies lässt Ihre defensive Praxis im Code - und konkreter - dokumentiert als eine allgemeine Richtlinie oder Praxis.

Zeiger sollte nur verwendet werden, wenn Sie etwas mit dem Zeiger tun müssen. Wie Zeiger arithmetisch, um einige Datenstruktur zu quer. Wenn möglich, sollte dies in einer Klasse eingekapselt werden.

Wenn der Zeiger in die Funktion übergeben wird, um etwas mit dem Objekt zu tun, auf das er verweist, geben Sie stattdessen eine Referenz über.

Eine Methode zur defensiven Programmierung besteht darin, fast alles zu behaupten, was Sie können. Zu Beginn des Projekts ist es ärgerlich, aber später ist es eine gute Ergänzung zu Unit -Tests.

Eine Reihe von Antwort befasst sich mit der Frage, wie Sie Abwehrkräfte in Ihren Code schreiben können, aber es wurde nicht viel darüber gesagt: "Wie defensiv sollten Sie sein?". Das müssen Sie anhand der Kritikalität Ihrer Softwarekomponenten bewerten.

Wir machen Flugsoftware und die Auswirkungen eines Softwarefehlers reichen von einem geringfügigen Ärger bis hin zum Verlust von Flugzeugen/Crew. Wir kategorisieren verschiedene Software -Teile anhand ihrer potenziellen nachteiligen Auswirkungen, die sich auf die Codierungsstandards, das Testen usw. auswirken. Sie müssen bewerten, wie Ihre Software verwendet wird, und die Auswirkungen von Fehlern und festlegen, welche Abwehrgrad Sie möchten (und können es sich leisten können). Das DO-178B Standard Ruft dieses "Design Assurance Level" auf.

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top