Namensnennung: Dies ist aus einer verwandten P.SE-Frage hervorgegangen
Mein Hintergrund ist C / C ++, aber ich habe ziemlich viel in Java gearbeitet und codiere derzeit C #. Aufgrund meines C-Hintergrunds ist das Überprüfen von übergebenen und zurückgegebenen Zeigern aus zweiter Hand, aber ich erkenne an, dass dies meine Sichtweise verzerrt.
Kürzlich wurde das Null-Objektmuster erwähnt, bei dem die Idee besteht, dass ein Objekt immer zurückgegeben wird. Normaler Fall gibt das erwartete, aufgefüllte Objekt zurück und der Fehlerfall gibt ein leeres Objekt anstelle eines Nullzeigers zurück. Die Voraussetzung ist, dass die aufrufende Funktion immer ein Objekt hat, auf das zugegriffen werden kann, und daher Verstöße gegen den Nullzugriffsspeicher zu vermeiden.
- Was sind die Vor- / Nachteile einer Nullprüfung gegenüber der Verwendung des Nullobjektmusters?
Ich kann mit dem NOP saubereren Aufrufcode sehen, aber ich kann auch sehen, wo es versteckte Fehler verursachen würde, die sonst nicht ausgelöst werden. Ich würde es vorziehen, wenn meine Anwendung während der Entwicklung hart ausfällt (auch bekannt als Ausnahme), anstatt einen stillen Fehler zu machen.
- Kann das Null-Objektmuster nicht ähnliche Probleme haben wie das Nichtdurchführen einer Nullprüfung?
Viele der Objekte, mit denen ich gearbeitet habe, enthalten Objekte oder eigene Container. Es scheint, als müsste ich einen Sonderfall haben, um zu gewährleisten, dass alle Container des Hauptobjekts eigene leere Objekte hatten. Scheint so, als könnte dies bei mehreren Nesting-Schichten hässlich werden.
quelle
Antworten:
Sie würden kein Null-Objektmuster an Stellen verwenden, an denen Null (oder Null-Objekt) 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 abbrechen (wie ich bereits sagte, in Fällen, in denen es keine Wiederherstellung gibt), aber Ihre Fehlerbehandlung maskiert sehr wichtige Informationen, die ein Absturzabbild geliefert hätte.
Null-Objektmuster ist eher für Orte gedacht, an denen ein Standardverhalten vorliegt, das in einem Fall auftreten kann, in dem ein Objekt nicht gefunden wird. Betrachten Sie zum Beispiel Folgendes:
Wenn Sie NOP verwenden, würden Sie schreiben:
Beachten Sie, dass sich dieser Code wie folgt verhält: "Wenn Bob existiert, möchte ich seine Adresse aktualisieren." Wenn für Ihre Anwendung die Anwesenheit von Bob erforderlich ist, möchten Sie offensichtlich nicht automatisch erfolgreich sein. Es gibt jedoch Fälle, in denen diese Art von Verhalten angemessen wäre. Und produziert NOP in solchen Fällen keinen saubereren und präziseren Code?
An Orten, an denen Sie wirklich nicht ohne Bob leben können, würde GetUser () eine Anwendungsausnahme auslösen (dh keine Zugriffsverletzung oder ähnliches), die auf einer höheren Ebene behandelt wird und einen allgemeinen Betriebsfehler meldet. 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 nur den Code und beeinträchtigen die Lesbarkeit. Auf NULL prüfen ist für einige Schnittstellen immer noch die richtige Design-Wahl, aber bei weitem nicht so viele, wie manche denken.
quelle
Vorteile
Nachteile
Dieser letzte "Profi" ist nach meiner Erfahrung das Hauptunterscheidungsmerkmal, wann jeder angewendet werden sollte. Msgstr "Sollte der Fehler laut sein?" In einigen Fällen möchten Sie, dass ein Fehler hart und sofort auftritt. wenn ein Szenario, das niemals passieren sollte, es irgendwie tut. Wenn eine wichtige Ressource nicht gefunden wurde ... usw. In einigen Fällen ist eine vernünftige Standardeinstellung in Ordnung, da es sich nicht wirklich um einen Fehler handelt : Abrufen eines Werts aus einem Wörterbuch, aber der Schlüssel fehlt beispielsweise.
Wie bei jeder anderen Designentscheidung gibt es je nach Ihren Anforderungen Vor- und Nachteile.
quelle
Ich bin keine gute Quelle für objektive Informationen, aber subjektiv:
In Umgebungen wie Java oder C # bietet dies nicht den Hauptvorteil von Explicitness - die Sicherheit der Lösung ist vollständig, da Sie an der Stelle eines Objekts im System immer noch Null erhalten können und Java keine Mittel hat (außer benutzerdefinierten Annotationen für Beans) , etc.), um Sie davor zu schützen, mit Ausnahme expliziter Ifs. In einem System, das frei von Null-Implementierungen ist, bietet eine solche Annäherung an die Problemlösung (mit Objekten, die Ausnahmefälle darstellen) 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 werden Null- / Fehlerobjekte / Rückgabetypen als sehr solide Fehlerbehandlungsstrategie und recht mathematisch fundiert angesehen, während die Ausnahmebehandlungsstrategie von Java oder C nur einen Schritt über der "die ASAP" -Strategie liegt - lassen Sie also im Grunde die Bürde von Instabilität und Unvorhersehbarkeit des Systems auf Entwickler oder Betreuer.
Es gibt auch Monaden, die dieser Strategie überlegen sind (auch in Bezug auf die Komplexität des Verständnisses zunächst überlegen), aber diese betreffen eher die 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-Objektmuster und der stillen Behandlung von Fehlersituationen gibt. Es sollte anders herum 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 auslassen, aber generisch behandeln - nun, das war Ihre explizite Wahl.
quelle
Ich finde es interessant festzustellen, dass die Lebensfähigkeit eines Null-Objekts ein wenig anders zu sein scheint, je nachdem, ob Ihre Sprache statisch typisiert ist oder nicht. Ruby ist eine Sprache, die ein Objekt für Null (oder
Nil
in der Terminologie der Sprache) implementiert .Anstatt einen Zeiger auf den nicht initialisierten Speicher zurückzugeben, gibt die Sprache das Objekt zurück
Nil
. Dies wird durch die dynamische Typisierung der Sprache erleichtert. Es ist nicht garantiert, dass eine Funktion immer ein zurückgibtint
. Es kann zurückkehren,Nil
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 aller Objekte zu implementieren, die Null sein könnten (oder die Scala / Haskell-Route einschlagen und eine Art "Vielleicht" -Hülle haben).MrLister hat darauf hingewiesen, dass Sie in C ++ beim erstmaligen Erstellen keinen Initialisierungsverweis / Zeiger haben. Indem Sie ein dediziertes Null-Objekt anstelle eines rohen Null-Zeigers haben, können Sie einige nette zusätzliche Funktionen erhalten. Ruby's
Nil
enthält eineto_s
(toString) -Funktion, die zu leerem Text führt. Dies ist großartig, wenn es Ihnen nichts ausmacht, eine Null-Rückgabe für einen String zu erhalten, und Sie die Berichterstellung überspringen möchten, ohne einen Absturz zu verursachen. Mit Open Classes können Sie auch die Ausgabe vonNil
notfalls manuell überschreiben .Zugegeben, dies kann alles mit einem Körnchen Salz eingenommen werden. Ich glaube, dass in einer dynamischen Sprache das Null-Objekt eine große Bequemlichkeit sein kann, wenn es richtig verwendet wird. Um das Beste aus dem Programm herauszuholen, müssen Sie möglicherweise die Basisfunktionalität bearbeiten. Dies kann dazu führen, dass Ihr Programm für Benutzer, die mit den Standardformularen arbeiten, nur schwer zu verstehen und zu warten ist.
quelle
Schauen Sie sich für einen zusätzlichen Standpunkt Objective-C an, bei dem anstelle eines Null-Objekts der Wert nil wie ein Objekt funktioniert. Jede Nachricht, die an ein Null-Objekt gesendet wird, hat keine Auswirkung und gibt den Wert 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
und erhalten Sie ein angemessenes Ergebnis, wenn aString == nil, weil eine nicht vorhandene Zeichenfolge keine Zeichen enthält. Die Leute brauchen eine Weile, um sich daran zu gewöhnen, aber ich würde wahrscheinlich eine Weile brauchen, um mich daran zu gewöhnen, ü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.)
quelle
nil
get#define
'd to NULL und verhält sich wie ein Null-Objekt. Dies ist eine Laufzeitfunktion, während in C # / Java Nullzeiger Fehler ausgeben.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, repräsentieren Sie einen potenziell standardmäßigen Wert mit einem anderen Typ 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).
grep
die Suche nach möglichen Fehlern.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) streitig.
Betrachten wir diese Sprache nach Sprache:
C ++
Meiner Meinung nach sollte eine C ++ - Klasse im Allgemeinen standardmäßig konstruierbar sein, da sie die Interaktion mit Bibliotheken erleichtert und die Verwendung der Klasse in Containern erleichtert. Dies vereinfacht auch die Vererbung, da Sie nicht darüber nachdenken müssen, welcher Superklassenkonstruktor aufgerufen werden soll.
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 Siebool nonempty
zur Laufzeit ein oder ein ähnliches Feld für die Oberflächen-Standardisierung eingeben, können Sie neue Instanzen von so erstellenMyClass
, dass der Benutzer gezwungen ist, dies zu überprüfen .Ich würde empfehlen, eine Factory-Funktion zu verwenden, die einen oder, wenn möglich
std::optional<MyClass>
,std::pair<bool, MyClass>
einen r-Wert-Verweis auf einen zurückgibtstd::unique_ptr<MyClass>
.Wenn Sie möchten, dass Ihre Factory-Funktion einen anderen "Platzhalter" -Zustand als den standardmäßig erstellten zurückgibt
MyClass
, verwenden Sie a,std::pair
und vergewissern Sie sich, dass Ihre Funktion dies tut.Wenn die Werksfunktion einen eindeutigen Namen hat, ist es einfach,
grep
nach dem Namen zu suchen und nach Schlampigkeit zu suchen. Es ist jedoch schwierig,grep
Fälle zu ermitteln, in denen der Programmierer die Werksfunktion hätte verwenden sollen, dies jedoch nicht tat .OCaml
Wenn Sie eine Sprache wie OCaml verwenden, können Sie einfach einen
option
Typ (in der OCaml-Standardbibliothek) oder eineneither
Typ (nicht in der Standardbibliothek, aber einfach zu rollen) verwenden. Oder eindefaultable
Typ (ich erfinde den Begriffdefaultable
).Ein
defaultable
wie oben gezeigtes ist besser als ein Paar, da der Benutzer eine Musterübereinstimmung durchführen muss, um das zu extrahieren,'a
und das erste Element des Paares nicht einfach ignorieren kann.Das
defaultable
oben gezeigte Äquivalent des Typs kann in C ++ unter Verwendung von astd::variant
mit zwei Instanzen desselben Typs verwendet werden, Teile derstd::variant
API können jedoch nicht verwendet werden, wenn beide Typen identisch sind. Es ist auch eine seltsame Verwendung,std::variant
da seine 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 bestimmten Typs benötigen, um den Compiler zu platzieren.
Ich würde nur empfehlen, eine Ausnahme auszulösen, wenn Sie gezwungen wären, eine Standardinstanz zu erstellen.
Wenn dies nicht akzeptabel ist, würde ich empfehlen, ein zu erstellen
DefaultedMyClass
,MyClass
das von Ihrer werkseitigen Funktion erbt, und dieses zurückzusenden. Das verschafft Ihnen Flexibilität in Bezug auf die "Stubbing" -Funktionalität in Standardinstanzen, wenn Sie dies benötigen.quelle