it-swarm.com.de

Nullzeiger vs. Nullobjektmuster

Namensnennung: Dies entstand aus einer verwandten P.SE-Frage

Mein Hintergrund ist in C/C++, aber ich habe eine ganze Menge in Java] gearbeitet und codiere derzeit C #. Aufgrund meines C-Hintergrunds ist das Überprüfen übergebener und zurückgegebener Zeiger aus zweiter Hand, aber ich Ich erkenne an, dass dies meinen Standpunkt beeinflusst.

Ich habe kürzlich die Erwähnung des Null Object Pattern gesehen, bei der die Idee ist, dass ein Objekt immer zurückgegeben wird. Der Normalfall gibt das erwartete, ausgefüllte Objekt zurück und der Fehlerfall gibt ein leeres Objekt anstelle eines Nullzeigers zurück. Die Voraussetzung ist, dass die aufrufende Funktion immer über ein Objekt verfügt, auf das zugegriffen werden kann, und daher Verletzungen des Nullzugriffsspeichers vermieden werden.

  • Was sind die Vor- und Nachteile einer Nullprüfung gegenüber der Verwendung des Nullobjektmusters?

Ich kann saubereren Aufrufcode mit dem NOP sehen, aber ich kann auch sehen, wo es versteckte Fehler verursachen würde, die sonst nicht ausgelöst werden. Ich möchte lieber, dass meine Anwendung während der Entwicklung schwer ausfällt (auch bekannt als Ausnahme), als dass ein stiller Fehler in die Wildnis entweicht.

  • Kann das Nullobjektmuster nicht ähnliche Probleme haben wie das Nichtdurchführen einer Nullprüfung?

Viele der Objekte, mit denen ich gearbeitet habe, enthalten eigene Objekte oder Container. Es scheint, als müsste ich einen Sonderfall haben, um sicherzustellen, dass alle Container des Hauptobjekts eigene leere Objekte hatten. Scheint so, als könnte dies mit mehreren Verschachtelungsebenen hässlich werden.

27
user53019

Sie würden kein Nullobjektmuster an Stellen verwenden, an denen Null (oder Nullobjekt) zurückgegeben wird, weil ein katastrophaler Fehler aufgetreten ist. An diesen Stellen würde ich weiterhin null zurückgeben. In einigen Fällen können Sie auch abstürzen, wenn keine Wiederherstellung erfolgt, da zumindest der Absturzspeicherauszug genau angibt, wo das Problem aufgetreten ist. In solchen Fällen, wenn Sie Ihre eigene Fehlerbehandlung hinzufügen, werden Sie den Prozess immer noch beenden (wieder sagte ich für Fälle, in denen es keine Wiederherstellung gibt), aber Ihre Fehlerbehandlung maskiert sehr wichtige Informationen, die ein Crash-Dump bereitgestellt hätte.

Das Nullobjektmuster ist eher für Orte gedacht, an denen ein Standardverhalten vorliegt, das in einem Fall angewendet werden kann, in dem kein Objekt gefunden wird. Betrachten Sie zum Beispiel Folgendes:

User* pUser = GetUser( "Bob" );

if( pUser )
{
    pUser->SetAddress( "123 Fake St." );
}

Wenn Sie NOP verwenden, würden Sie schreiben:

GetUser( "Bob" )->SetAddress( "123 Fake St." );

Beachten Sie, dass dieser Code "Wenn Bob existiert, möchte ich seine Adresse aktualisieren" lautet. Wenn Ihre Bewerbung erfordert, dass Bob anwesend ist, möchten Sie natürlich nicht stillschweigend erfolgreich sein. Es gibt jedoch Fälle, in denen diese Art von Verhalten angemessen wäre. Und produziert NOP in diesen Fällen nicht einen viel saubereren und präziseren Code?

An Orten, an denen Sie ohne Bob wirklich nicht leben können, würde GetUser () eine Anwendungsausnahme auslösen (d. H. Keine Zugriffsverletzung oder ähnliches), die auf einer höheren Ebene behandelt würde und einen allgemeinen Betriebsfehler melden würde. In diesem Fall ist NOP nicht erforderlich, es muss jedoch auch nicht explizit nach NULL gesucht werden. IMO, diese Überprüfungen auf NULL, vergrößern den Code nur und beeinträchtigen die Lesbarkeit. Die Überprüfung auf NULL ist für einige Schnittstellen immer noch die richtige Wahl, aber bei weitem nicht so viele, wie manche Leute denken.

25
DXM

Was sind die Vor- und Nachteile einer Nullprüfung gegenüber der Verwendung des Nullobjektmusters?

Vorteile

  • Eine Nullprüfung ist besser, da sie mehr Fälle löst. Nicht alle Objekte haben ein vernünftiges Standard- oder No-Op-Verhalten.
  • Eine Nullprüfung ist solider. Sogar Objekte mit vernünftigen Standardeinstellungen werden an Orten verwendet, an denen der vernünftige Standard nicht gültig ist. Code sollte in der Nähe der Grundursache fehlschlagen, wenn er fehlschlagen soll. Code sollte offensichtlich fehlschlagen, wenn er fehlschlagen soll.

Nachteile

  • Vernünftige Standardeinstellungen führen normalerweise zu saubererem Code.
  • Vernünftige Standardeinstellungen führen normalerweise zu weniger katastrophalen Fehlern, wenn sie es schaffen, in die Wildnis zu gelangen.

Dieser letzte "Profi" ist (meiner Erfahrung nach) das Hauptunterscheidungsmerkmal, wann jeder angewendet werden sollte. "Sollte der Fehler laut sein?". In einigen Fällen möchten Sie, dass ein Fehler hart und unmittelbar auftritt. wenn ein Szenario, das niemals passieren sollte, dies tut. Wenn eine wichtige Ressource nicht gefunden wurde ... usw. In einigen Fällen ist ein vernünftiger Standard in Ordnung, da es sich nicht wirklich um einen Fehler handelt : einen Wert aus einem Wörterbuch abrufen, aber der Schlüssel fehlt zum Beispiel.

Wie bei jeder anderen Designentscheidung gibt es je nach Ihren Anforderungen Vor- und Nachteile.

7
Telastyn

Ich bin keine gute Quelle für objektive Informationen, aber subjektiv:

  • Ich möchte nicht, dass mein System jemals stirbt. Für mich ist es ein Zeichen für ein schlecht entworfenes oder unvollständiges System. Das gesamte System sollte alle möglichen Fälle behandeln und niemals in einen unerwarteten Zustand geraten. Um dies zu erreichen, muss ich alle Abläufe und Situationen explizit modellieren. Die Verwendung von Nullen ist ohnehin nicht explizit und gruppiert viele verschiedene Fälle unter einem Dach. Ich bevorzuge das NonExistingUser-Objekt gegenüber null, ich bevorzuge NaN gegenüber null, ... ein solcher Ansatz ermöglicht es mir, diese Situationen und ihre Behandlung in so vielen Details wie ich möchte explizit zu beschreiben, während null mir nur die Option null lässt. In einer Umgebung wie Java kann jedes Objekt null sein. Warum sollten Sie sich also lieber in diesem generischen Pool von Fällen auch in Ihrem speziellen Fall verstecken?
  • null-Implementierungen haben ein drastisch anderes Verhalten als Objekte und werden meistens auf die Menge aller Objekte geklebt (warum? warum? warum?); Für mich scheint ein derart drastischer Unterschied ein Ergebnis des Entwurfs von Nullen zu sein, das ein Hinweis auf einen Fehler ist. In diesem Fall wird das System höchstwahrscheinlich sterben (ich nehme an, dass so viele Leute es tatsächlich vorziehen, dass ihr System in den obigen Beiträgen stirbt), sofern dies nicht ausdrücklich behandelt wird. Für mich ist das ein fehlerhafter Ansatz in seiner Wurzel - es erlaubt dem System, standardmäßig zu sterben, es sei denn, Sie haben sich ausdrücklich darum gekümmert, das ist nicht sehr sicher, oder? In jedem Fall ist es jedoch unmöglich, einen Code zu schreiben, der den Wert null und das Objekt auf dieselbe Weise behandelt. Es funktionieren nur wenige grundlegende integrierte Operatoren (zuweisen, gleich, param usw.), der gesamte andere Code schlägt einfach fehl und Sie benötigen ihn zwei völlig unterschiedliche Wege;
  • verwenden von Null-Objekt-Skalen besser - Sie können so viele Informationen und Strukturen einfügen, wie Sie möchten. NonExistingUser ist ein sehr gutes Beispiel - es kann eine E-Mail des Benutzers enthalten, den Sie empfangen möchten, und vorschlagen, basierend auf diesen Informationen einen neuen Benutzer zu erstellen. Bei der Nulllösung müsste ich mir überlegen, wie die E-Mail beibehalten werden soll, auf die die Benutzer zugreifen wollten nahe an der Null-Ergebnisbehandlung;

In einer Umgebung wie Java oder C # bietet dies nicht den Hauptvorteil der expliziten Aussage - die Sicherheit der Lösung ist vollständig, da Sie anstelle eines Objekts im System und = weiterhin null erhalten können Java hat keine Möglichkeit (außer benutzerdefinierten Annotationen für Beans usw.), Sie davor zu schützen, außer expliziten ifs. Aber im System, das frei von Null-Implementierungen ist, nähern Sie sich der Problemlösung auf diese Weise (mit Objekte, die Ausnahmefälle darstellen) bieten alle genannten Vorteile. Versuchen Sie also, etwas anderes als die gängigen Sprachen zu lesen, und Sie werden feststellen, dass Sie Ihre Codierungsmethoden ändern.

Im Allgemeinen wird Null/Fehlerobjekte/Rückgabetypen als sehr solide Fehlerbehandlungsstrategie und mathematisch fundiert angesehen, während die Ausnahmebehandlungsstrategie von Java oder C wie nur ein Schritt über "die ASAP" geht). Strategie - Überlassen Sie daher im Grunde genommen die Last der Instabilität und Unvorhersehbarkeit des Systems dem Entwickler oder Betreuer.

Es gibt auch Monaden, die dieser Strategie überlegen sind (zunächst auch in der Komplexität des Verständnisses überlegen), aber es geht eher um Fälle, in denen Sie externe Aspekte des Typs modellieren müssen, während Null Object interne Aspekte modelliert. Daher ist NonExistingUser wahrscheinlich besser als monad Maybe_existing modelliert.

Und schließlich glaube ich nicht, dass es einen Zusammenhang zwischen dem Null-Objekt-Muster und der stillschweigenden Behandlung von Fehlersituationen gibt. Es sollte umgekehrt sein - es sollte Sie zwingen, jeden einzelnen Fehlerfall explizit zu modellieren und entsprechend zu behandeln. Jetzt könnten Sie sich natürlich dafür entscheiden, schlampig zu sein (aus welchem ​​Grund auch immer) und die Behandlung des Fehlerfalls explizit überspringen, aber generisch behandeln - nun, das war Ihre explizite Wahl.

6
Bohdan Tsymbala

Ich finde es interessant festzustellen, dass die Lebensfähigkeit eines Null-Objekts ein wenig unterschiedlich zu sein scheint, je nachdem, ob Ihre Sprache statisch typisiert ist oder nicht. Ruby ist eine Sprache, die ein Objekt für Null implementiert (oder Nil in der Terminologie der Sprache).

Anstatt einen Zeiger auf den nicht initialisierten Speicher zurückzugewinnen, gibt die Sprache das Objekt Nil zurück. Dies wird durch die Tatsache erleichtert, dass die Sprache dynamisch typisiert wird. Es ist nicht garantiert, dass eine Funktion immer ein int zurückgibt. Es kann Nil zurückgeben, wenn es das ist, was es tun muss. Es wird komplizierter und schwieriger, dies in einer statisch typisierten Sprache zu tun, da Sie natürlich erwarten, dass null auf jedes Objekt anwendbar ist, das als Referenz aufgerufen werden kann. Sie müssten damit beginnen, Nullversionen eines Objekts zu implementieren, das null sein könnte (oder die Scala/Haskell-Route wählen und eine Art "Vielleicht" -Wrapper haben).

MrLister brachte die Tatsache zur Sprache, dass in C++ nicht garantiert wird, dass Sie beim ersten Erstellen eine Initialisierungsreferenz/einen Initialisierungszeiger haben. Wenn Sie ein dediziertes Nullobjekt anstelle eines rohen Nullzeigers haben, können Sie einige nette zusätzliche Funktionen erhalten. Rubys Nil enthält ein to_s (toString) -Funktion, die zu leerem Text führt. Dies ist großartig, wenn es Ihnen nichts ausmacht, eine Null-Rückgabe für eine Zeichenfolge zu erhalten, und Sie die Berichterstellung ohne Absturz überspringen möchten. Mit Open Classes können Sie die Ausgabe von Nil bei Bedarf auch manuell überschreiben.

Zugegeben, das alles kann mit einem Körnchen Salz eingenommen werden. Ich glaube, dass in einer dynamischen Sprache das Null-Objekt bei korrekter Verwendung eine große Bequemlichkeit sein kann. Um das Beste daraus zu machen, müssen Sie möglicherweise die Basisfunktionalität bearbeiten, was dazu führen kann, dass Ihr Programm für Benutzer, die mit den Standardformularen arbeiten, schwer zu verstehen und zu warten ist.

3
KChaloux

Für einen zusätzlichen Gesichtspunkt werfen Sie einen Blick auf Objective-C, wo anstelle eines Null-Objekts der Wert nil wie ein Objekt funktioniert. Jede an ein nil-Objekt gesendete Nachricht hat keine Auswirkung und gibt einen Wert von 0/0.0/NO (false)/NULL/nil zurück, unabhängig vom Rückgabewerttyp der Methode. Dies wird in der MacOS X- und iOS-Programmierung als weit verbreitetes Muster verwendet. In der Regel werden Methoden so entworfen, dass ein Null-Objekt, das 0 zurückgibt, ein vernünftiges Ergebnis ist.

Wenn Sie beispielsweise überprüfen möchten, ob eine Zeichenfolge ein oder mehrere Zeichen enthält, können Sie schreiben

aString.length > 0

und erhalten Sie ein vernünftiges Ergebnis, wenn aString == nil ist, da eine nicht vorhandene Zeichenfolge keine Zeichen enthält. Die Leute brauchen eine Weile, um sich daran zu gewöhnen, aber es würde wahrscheinlich eine Weile dauern, bis ich mich daran gewöhnt habe, überall in anderen Sprachen nach Nullwerten zu suchen.

(Es gibt auch ein Singleton-Objekt der Klasse NSNull, das sich ganz anders verhält und in der Praxis nicht sehr häufig verwendet wird.).

1
gnasher729

Verwenden Sie auch nicht, wenn Sie dies vermeiden können. Verwenden Sie eine Factory-Funktion, die einen Optionstyp, ein Tupel oder einen dedizierten Summentyp zurückgibt. Mit anderen Worten, stellen Sie einen potenziell voreingestellten Wert mit einem anderen Typ dar als einen garantierten nicht standardmäßigen Wert.

Lassen Sie uns zuerst die Desiderata auflisten und dann darüber nachdenken, wie wir dies in einigen Sprachen ausdrücken können (C++, OCaml, Python).

  1. fangen Sie beim Kompilieren die unerwünschte Verwendung eines Standardobjekts ab.
  2. machen Sie beim Lesen des Codes deutlich, ob ein bestimmter Wert möglicherweise voreingestellt ist oder nicht.
  3. wählen Sie gegebenenfalls unseren sinnvollen Standardwert einmal pro Typ. Auf keinen Fall wählen Sie an jedem Anrufort einen möglicherweise anderen Standard.
  4. machen Sie es statischen Analysewerkzeugen oder einem Menschen mit grep leicht, nach möglichen Fehlern zu suchen.
  5. Für einige Anwendungen sollte das Programm normal fortgesetzt werden, wenn unerwartet ein Standardwert angegeben wird. Für andere Anwendungen sollte das Programm sofort angehalten werden, wenn ein Standardwert angegeben wird, idealerweise auf informative Weise.

Ich denke, die Spannung zwischen dem Nullobjektmuster und den Nullzeigern kommt von (5). Wenn wir Fehler jedoch früh genug erkennen können, wird (5) strittig.

Betrachten wir diese Sprache für Sprache:

C++

Meiner Meinung nach sollte eine C++ - Klasse im Allgemeinen standardmäßig konstruierbar sein, da dies die Interaktion mit Bibliotheken erleichtert und die Verwendung der Klasse in Containern erleichtert. Es vereinfacht auch die Vererbung, da Sie nicht darüber nachdenken müssen, welchen Superklassenkonstruktor Sie aufrufen sollen.

Dies bedeutet jedoch, dass Sie nicht sicher wissen können, ob sich ein Wert vom Typ MyClass im "Standardzustand" befindet oder nicht. Abgesehen davon, dass Sie zur Laufzeit ein bool nonempty Oder ähnliches Feld für die Oberflächenstandardisierung eingeben, können Sie am besten neue Instanzen von MyClass auf eine Weise erstellen, die den Benutzer zur Überprüfung zwingt it.

Ich würde empfehlen, eine Factory-Funktion zu verwenden, die nach Möglichkeit einen std::optional<MyClass>, std::pair<bool, MyClass> Oder einen R-Wert-Verweis auf einen std::unique_ptr<MyClass> Zurückgibt.

Wenn Sie möchten, dass Ihre Factory-Funktion einen "Platzhalter" -Status zurückgibt, der sich von einem standardmäßig erstellten MyClass unterscheidet, verwenden Sie einen std::pair Und dokumentieren Sie, dass Ihre Funktion dies tut.

Wenn die Factory-Funktion einen eindeutigen Namen hat, ist es einfach, grep nach dem Namen zu suchen und nach Schlamperei zu suchen. Es ist jedoch schwierig, grep für Fälle zu verwenden, in denen der Programmierer hätte die Factory-Funktion verwenden sollen, dies aber nicht getan hat.

OCaml

Wenn Sie eine Sprache wie OCaml verwenden, können Sie einfach einen Typ option (in der OCaml-Standardbibliothek) oder einen Typ either (nicht in der Standardbibliothek, aber einfach zu rollen) verwenden besitzen). Oder ein defaultable Typ (ich mache den Begriff defaultable).

type 'a option =
    | None
    | Some of 'a

type ('a, 'b) either =
    | Left of 'a
    | Right of 'b

type 'a defaultable =
    | Default of 'a
    | Real of 'a

Ein defaultable wie oben gezeigt ist besser als ein Paar, da der Benutzer eine Musterübereinstimmung durchführen muss, um den 'a Zu extrahieren, und das erste Element des Paares nicht einfach ignorieren kann.

Das oben gezeigte Äquivalent des Typs defaultable kann in C++ unter Verwendung eines std::variant Mit zwei Instanzen desselben Typs verwendet werden. Teile der API std::variant Sind jedoch unbrauchbar, wenn beide Typen sind die gleichen. Es ist auch eine seltsame Verwendung von std::variant, Da die Typkonstruktoren unbenannt sind.

Python

Sie erhalten ohnehin keine Überprüfung zur Kompilierungszeit für Python. Bei einer dynamischen Typisierung gibt es jedoch im Allgemeinen keine Umstände, unter denen Sie eine Platzhalterinstanz eines Typs benötigen, um den Compiler zu beruhigen.

Ich würde empfehlen, nur eine Ausnahme auszulösen, wenn Sie gezwungen wären, eine Standardinstanz zu erstellen.

Wenn dies nicht akzeptabel ist, würde ich empfehlen, ein DefaultedMyClass zu erstellen, das von MyClass erbt, und dieses von Ihrer Factory-Funktion zurückzugeben. Dies verschafft Ihnen Flexibilität in Bezug auf das "Ausblenden" von Funktionen in Standardinstanzen, wenn dies erforderlich ist.

0
Gregory Nisbet