Verhindern von null oder Überprüfen auf null



  • Hallo,

    immer wenn ich zu Übungszwecken kleine Snippets erstelle, frage ich mich, wie die Sache mit null pointer / Referenzen zu behandeln ist.

    Sagen wir mal mein schreibt eine Klasse foo und diese Klasse benutzt eine andere Klasse, sagen wir mal Klasse A.

    Klasse A ist nicht internes der foo, sondern der Nutzer soll durchaus festlegen dürfen, welche Eigenschaften A in foo hat.
    Beispielsweise über den Konstruktor von foo eine A-Instanz übergeben, mit der dann der entsprechende Member der foo-Instanz initialisiert werden.

    So, jetzt bietet die Klasse foo einige Methoden an, in denen mit Hilfe von der A-Membervariable etwas berechnet / verändert, etc. wird.

    Prüfe ich jetzt in jeder Methode der Klasse foo, die auf A zugreift, ob A != null, oder verhindere ich konsistent in der gesamten foo-Klasse, dass A jemals null wird?(Beispielsweie indem ich die übergebenen Werte im Konstrukor prüfe etc.)

    Oder mache ich gar nichts und lasse den User der Klasse foo leiden, dafür, dass er erlaubt hat, dass A null wurde? 😮



  • Du könntest das Nullobjekt (Entwurfsmuster) benutzen, d.h. sobald für A null übergeben wird, setzt du intern ein NullObjekt und greifst dann in deinen Methoden ohne Prüfung auf die Member zu.



  • Hmm, verstehe.

    Also im Endeffekt überprüfe ich lieber das, was für A übergeben wird, anstatt an allen nötigen Stellen immer zu schauen, ob A in zulässiger Verfassung ist?

    Ob ich jetzt dann A auf so ein NullObjekt setze, oder einfache Default-Instanz übergebe, ist ja dann nicht so wirklich ausschlaggebend.

    Ich denke das Design bricht nur, wenn man irgendwann nicht mehr garantieren kann, dass A != null bleibt, aber ich denke es macht wenig Sinn für solche theoretischen Fälle "vorzuentwickeln".

    Also ich halte fest: Lieber in foo garantieren, dass A nie null wird und dafür dann in allen Methoden bedenkenlos drauf zugreifen?



  • Ich sehe drei Möglichkeiten die alle Sinn machen können:

    1. Du erlaubst die Nutzung bestimmter Funktionen mit a == null .
      Dann musst du in allen Funktionen wo a benötigt wird prüfen ob a == null , und dann ne "das geht so nicht" Exception werfen.
      Kann Sinn machen wenn das Erzeugen eines A Objekts recht umständlich und/oder teuer ist, und der Fall dass nur die "geht ohne a" Funktionen benötigt werden häufig eintreten wird.
      Ist allerdings unschön, und vermutlich gäbe es ein schöneres Design - z.B. Trennung in zwei oder mehr Klassen.

    2. Wenn du ein sinnvolles Default A Objekt erzeugen kannst, kannst du genau das machen wenn a == null im Konstruktor übergeben wird. Macht Sinn, wenn es eben sinnvolle Defaults gibt. Also wenn man davon ausgehen kann dass viele Nutzer diese Defaults verwenden können.

    3. Du wirfst einfach ne "argument is null" Exception im Konstruktor wenn a == null übergeben wird.



  • Alles klar, ich denke in dem Fall bleib ich jetzt erstmal bei Option 3).

    Die anderen beiden Varianten verstehe ich auch und ich kann mir durchaus vorstellen, dass sie auch zum Einsatz kommen.
    Nur in dem Fall, da sowieso so simpel, belasse ich es bei dem Verbot, A auf null zu setzen 🙂

    Vielen Dank an Euch beide! 👍



  • 4. assert( a != nullptr );



  • @NPE
    Und wirf auf jeden Fall den passenden Exception-Typ.
    Bei .NET wäre das ArgumentNullException .
    Bei Java gibt's vermutlich was ähnliches.
    Bei C++ wäre es wohl std::invalid_argument .



  • DocShoe schrieb:

    4. assert( a != nullptr );

    Das hatte ich auch anfangs, stimmt. Ist in etwa äquivalent zur Exception? Oder etwas ... drastischer, vielleicht? 😛

    hustbaer schrieb:

    @NPE
    Und wirf auf jeden Fall den passenden Exception-Typ.
    Bei .NET wäre das ArgumentNullException .
    Bei Java gibt's vermutlich was ähnliches.
    Bei C++ wäre es wohl std::invalid_argument .

    Jop, ist in dem Fall bei Java, da habe ich jetzt einfach mal die IllegalArgumentException verwendet.
    Hab auch mal den Konstruktor mit "throws ..." deklariert, ob das nötig ist weiß ich aber nicht.



  • NPE schrieb:

    Das hatte ich auch anfangs, stimmt. Ist in etwa äquivalent zur Exception? Oder etwas ... drastischer, vielleicht? 😛

    Es ist eigentlich etwas ganz anderes: Es sagt: null zu übergeben ist ein Logikfehler. Während Exception sagt: es ist ein Fehler der vorkommen kann und wir müssen jederzeit damit rechnen dass man null übergeben kann.

    Also Grundlegend schon 2 unterschiedliche Sachen.



  • Shade Of Mine schrieb:

    NPE schrieb:

    Das hatte ich auch anfangs, stimmt. Ist in etwa äquivalent zur Exception? Oder etwas ... drastischer, vielleicht? 😛

    Es ist eigentlich etwas ganz anderes: Es sagt: null zu übergeben ist ein Logikfehler. Während Exception sagt: es ist ein Fehler der vorkommen kann und wir müssen jederzeit damit rechnen dass man null übergeben kann.

    Also Grundlegend schon 2 unterschiedliche Sachen.

    Verstehe ... In diesem Fall würde ich dann doch eher die Exception bevorzugen. Das assert klingt dann schon stark drastisch.

    Danke 🙂



  • NPE schrieb:

    DocShoe schrieb:

    4. assert( a != nullptr );

    Das hatte ich auch anfangs, stimmt. Ist in etwa äquivalent zur Exception? Oder etwas ... drastischer, vielleicht? 😛

    assert() sagt "das darfst du nicht machen". Nienicht niemalsnicht machen. Es wird allerdings nur in Debug Builds geprüft, und ist als Entwicklungs- bzw. Debug-Tool einzustufen. Bzw. dient zu Dokumentationszwecken. Es verhindert nichts in Release Builds.

    Exception werfen sagt "wenn du das machst schmeiss ich' dir ne Exception um die Ohren". Exception sollten normalerweise nie abhängig von Debug/Release geworfen oder nicht geworfen werden. Und dieses "wenn X dann werfe ich Y" stellt quasi ein Versprechen dar, einen "Contract". D.h. es kann sein dass sich jemand der deine Klasse verwendet darauf verlässt.

    Die beiden machen also etwas unterschiedliches. Und sie schliessen sich gegenseitig nicht aus.

    Wenn du absolut verhindern willst dass ein Objekt konstruiert wird das kein A hat, dann hilft assert nichts. In dem Fall musst du entweder den Konstruktor abbrechen (was nur geht indem du eine Exception wirfst), oder du musst selbst ein A aus irgend einem Hut ziehen.

    Und wenn du kommunizieren willst dass es ein "mach das niemals" Fehler ist den Konstruktor mit a == null aufzurufen, dann schreibst du ein assert() rein.

    In manchen Fällen macht es Sinn nur eines der beiden zu machen. z.B. kann es durchaus OK sein Werte die man selbst von anderswo her bekommt ungeprüft weiterzugeben, weil man weiss dass der Teil dem man es übergibt ne Exception wirft wenn etwas nicht passt. Das spart (redundanten) Code. Und redundanten Code sparen ist oft gut. In dem Fall wäre es dann unpraktisch wenn in deinem Konstruktor ein assert() steht. Weil das dann vielleicht immer fliegt wenn ein User eine ungültige Eingabe macht.

    In anderen Fällen kann man es aber als "das ist immer ein (Programmier)fehler" einstufen wenn irgendwo null (oder ein anderer unpassender Wert) daherkommt. Dort kann man dann assert() schreiben, um zu vermeiden dass Fehler beim Testen unerkannt bleiben. Bzw. dass der Fehler erst an einer Stelle erkannt wird wo man nicht mehr nachvollziehen kann was eigentlich passiert ist. Ob man dann noch eine zusätzliche Reaktion implementieren sollte, die auch in Release-Builds übrig bleibt, ist natürlich abhängig davon worum es genau geht. Und auch ein bisschen eine Glaubensfrage.
    Ich sage an Stellen wo es wichtig ist dass das Programm nie nie nienicht weiterläuft wenn die geprüfte Bedingung erfüllt ist sollte man irgend eine Reaktion implementieren die auch im Release bleibt. Entweder das Programm mit einer Fehlermeldung abbrechen, oder halt ne Exception werfen.

    TL;DR
    NPE hat es geschafft das wichtigste mit 1 1/2 Sätzen zusammenzufassen.



  • hustbaer schrieb:

    NPE schrieb:

    DocShoe schrieb:

    4. assert( a != nullptr );

    Das hatte ich auch anfangs, stimmt. Ist in etwa äquivalent zur Exception? Oder etwas ... drastischer, vielleicht? 😛

    assert() sagt "das darfst du nicht machen". Nienicht niemalsnicht machen. Es wird allerdings nur in Debug Builds geprüft, und ist als Entwicklungs- bzw. Debug-Tool einzustufen. Bzw. dient zu Dokumentationszwecken. Es verhindert nichts in Release Builds.

    Exception werfen sagt "wenn du das machst schmeiss ich' dir ne Exception um die Ohren". Exception sollten normalerweise nie abhängig von Debug/Release geworfen oder nicht geworfen werden. Und dieses "wenn X dann werfe ich Y" stellt quasi ein Versprechen dar, einen "Contract". D.h. es kann sein dass sich jemand der deine Klasse verwendet darauf verlässt.

    Die beiden machen also etwas unterschiedliches. Und sie schliessen sich gegenseitig nicht aus.

    Wenn du absolut verhindern willst dass ein Objekt konstruiert wird das kein A hat, dann hilft assert nichts. In dem Fall musst du entweder den Konstruktor abbrechen (was nur geht indem du eine Exception wirfst), oder du musst selbst ein A aus irgend einem Hut ziehen.

    Und wenn du kommunizieren willst dass es ein "mach das niemals" Fehler ist den Konstruktor mit a == null aufzurufen, dann schreibst du ein assert() rein.

    In manchen Fällen macht es Sinn nur eines der beiden zu machen. z.B. kann es durchaus OK sein Werte die man selbst von anderswo her bekommt ungeprüft weiterzugeben, weil man weiss dass der Teil dem man es übergibt ne Exception wirft wenn etwas nicht passt. Das spart (redundanten) Code. Und redundanten Code sparen ist oft gut. In dem Fall wäre es dann unpraktisch wenn in deinem Konstruktor ein assert() steht. Weil das dann vielleicht immer fliegt wenn ein User eine ungültige Eingabe macht.

    In anderen Fällen kann man es aber als "das ist immer ein (Programmier)fehler" einstufen wenn irgendwo null (oder ein anderer unpassender Wert) daherkommt. Dort kann man dann assert() schreiben, um zu vermeiden dass Fehler beim Testen unerkannt bleiben. Bzw. dass der Fehler erst an einer Stelle erkannt wird wo man nicht mehr nachvollziehen kann was eigentlich passiert ist. Ob man dann noch eine zusätzliche Reaktion implementieren sollte, die auch in Release-Builds übrig bleibt, ist natürlich abhängig davon worum es genau geht. Und auch ein bisschen eine Glaubensfrage.
    Ich sage an Stellen wo es wichtig ist dass das Programm nie nie nienicht weiterläuft wenn die geprüfte Bedingung erfüllt ist sollte man irgend eine Reaktion implementieren die auch im Release bleibt. Entweder das Programm mit einer Fehlermeldung abbrechen, oder halt ne Exception werfen.

    Vielen Dank für diese ausführliche Erklärung!

    Es war mir tatsächlich nicht bewusst, dass assert() nur auf Debug-Ebene Effekt hat - eine sehr wichtige Information.

    Ich glaub ich hab durch deine Beschreibung jetzt ein ganz gutes Gefühl, wann ich assert() benutzen wollen würde.

    Auch was meine ursprüngliche Frage angeht, bin ich denke ich auf einen ganz zufriedenstellenden Standpunkt gekommen.
    Sofern ich die Klasse entwickle und es nicht sinnvoll ist, dass gewisse Attribute gewisse Werte annehmen, dann verhindere ich das einfach, indem ich es dem Nutzer der Klasse verbiete.

    Ich hatte nur immer das Szenario im Kopf, dass x Entwickler an einer Klasse schrauben. Der 1. Entwickler hat die gleiche Idee wie ich, verhindert also in allen nötigen Methoden, dass gewisse Werte für gewisse Attribute übergeben werden und prüft als Folge dann nicht ein zweites mal in den Methoden, wo er diese verwendet.
    Dann kommt irgendwann der 10. Entwickler, erweitert die Klasse in gewissem Sinne und achtet nicht mehr auf die Idee vom 1. Entwickler. Und dann fliegen NPEs und so ein Salat, der 10. Entwickler denkt sich was für ein Heini der 1. ist, dass er nicht auf null überprüft hat etc.

    Aber das ist wohl eher menschlicher Fehler als fachlicher Fehler. Es wäre wohl sinnlos zu versuchen, solchen Szenarios vorzuwirken.

    Danke noch mal an alle 🙂



  • C++ vertritt die Philosophie, dass du nur für das bezahlst (Laufzeit), was du auch verwendest.

    Wenn dein Framework/Klasse NULL/nullptr für a nicht zulässt, dann bin ich für assert ohne eine Exception zu werfen. Das sollte dann aber auch in der Doku oder im Header der Klasse stehen. Wenn jemand NULL/nullptr übergibt ist das ein Fehler des jeweiligen Programmierers.
    Das Überprüfen auf Gültigkeit und das Werfen einer Exception kostet immer Laufzeit, wenn auch nur minimal. Aber jedes Mal. Wenn abzusehen ist, dass dieser Fall sehr häufig eintritt, z.b. einge zig-tausend mal pro Sekunde innerhalb einer Schleife, dann bezahlst du diesen Aufwand jedes Mal mit.

    Lange Rede, kurzer Sinn:
    Es gibt keine allgemein gültige Regel. Es hängt immer vom Einsatzzweck ab, und da musst du jetzt abwägen.

    PS:
    Es hängt natürlich auch davon ab, wo und wann der Zeiger benutzt wird. Nehmen wir als Beispiel diese Klasse:

    class SomeClass
    {
    public:
      void f( SomeOtherClass* obj )
      {
         g( obj );
         h( obj );
      }
    private:
      void g( SomeOtherClass* obj )
      {
          ...
      }
    
      void h( SomeOtherClass* obj )
      {
          ...
      }
    };
    

    Da g und h von außen überhaupt keinen Zugriff erlauben und nur von f benutzt werden würde ich in g und h maximal mit assert prüfen, ob die Zeiger gültig sind. Für f muss man jetzt wieder abwägen, welche Option man wählt, da kann eine Überprüfung und ggf. das Werfen einer Exception der bessere Weg sein.


Log in to reply