Ich denke, die kurze Zusammenfassung, warum Null unerwünscht ist, ist, dass bedeutungslose Zustände nicht darstellbar sein sollten .
Angenommen, ich modelliere eine Tür. Es kann sich in einem von drei Zuständen befinden: Öffnen, Schließen, aber Entsperren und Schließen und Sperren. Jetzt konnte ich es nach dem Vorbild von modellieren
class Door
private bool isShut
private bool isLocked
und es ist klar, wie ich meine drei Zustände diesen beiden booleschen Variablen zuordnen kann. Damit bleibt jedoch ein vierter, unerwünschter Zustand verfügbar : isShut==false && isLocked==true
. Da die Typen, die ich als meine Darstellung ausgewählt habe, diesen Zustand zulassen, muss ich mich mental anstrengen, um sicherzustellen, dass die Klasse niemals in diesen Zustand gelangt (möglicherweise durch explizite Codierung einer Invariante). Im Gegensatz dazu, wenn ich eine Sprache mit algebraischen Datentypen oder überprüften Aufzählungen verwendet habe, mit denen ich definieren kann
type DoorState =
| Open | ShutAndUnlocked | ShutAndLocked
dann könnte ich definieren
class Door
private DoorState state
und es gibt keine Sorgen mehr. Das Typsystem stellt sicher, dass nur drei mögliche Zustände für eine Instanz class Door
vorhanden sind. Dies ist der Typ, in dem Typsysteme gut sind - indem eine ganze Klasse von Fehlern beim Kompilieren explizit ausgeschlossen wird.
Das Problem dabei null
ist, dass jeder Referenztyp diesen zusätzlichen Status in seinem Bereich erhält, der normalerweise unerwünscht ist. Eine string
Variable kann eine beliebige Folge von Zeichen sein, oder es kann sich um diesen verrückten Zusatzwert handeln null
, der nicht in meine Problemdomäne passt. Ein Triangle
Objekt hat drei Point
s, die selbst X
und Y
Werte haben, aber leider kann das Point
s oder das Triangle
selbst dieser verrückte Nullwert sein, der für die Grafikdomäne, in der ich arbeite, bedeutungslos ist.
Wenn Sie beabsichtigen, einen möglicherweise nicht vorhandenen Wert zu modellieren, sollten Sie sich explizit dafür entscheiden. Wenn ich beabsichtige, Menschen zu modellieren, dass jeder Person
ein FirstName
und ein hat LastName
, aber nur einige Menschen ein MiddleName
s haben, dann möchte ich so etwas sagen
class Person
private string FirstName
private Option<string> MiddleName
private string LastName
wobei string
hier angenommen wird, dass es sich um einen nicht nullbaren Typ handelt. Dann sind keine kniffligen Invarianten zu ermitteln und keine unerwarteten NullReferenceException
s, wenn versucht wird, die Länge des Namens einer Person zu berechnen. Das Typsystem stellt sicher, dass jeder Code, der sich mit den MiddleName
Konten befasst, die Möglichkeit hat, dass er vorhanden ist None
, während jeder Code, der sich mit den Konten befasst, FirstName
sicher davon ausgehen kann, dass dort ein Wert vorhanden ist.
Mit dem obigen Typ könnten wir beispielsweise diese dumme Funktion erstellen:
let TotalNumCharsInPersonsName(p:Person) =
let middleLen = match p.MiddleName with
| None -> 0
| Some(s) -> s.Length
p.FirstName.Length + middleLen + p.LastName.Length
ohne Sorgen. Im Gegensatz dazu wird in einer Sprache mit nullbaren Referenzen für Typen wie Zeichenfolge angenommen
class Person
private string FirstName
private string MiddleName
private string LastName
Am Ende verfassen Sie Dinge wie
let TotalNumCharsInPersonsName(p:Person) =
p.FirstName.Length + p.MiddleName.Length + p.LastName.Length
Dies geht in die Luft, wenn das Objekt der eingehenden Person nicht die Invariante hat, dass alles nicht null ist, oder
let TotalNumCharsInPersonsName(p:Person) =
(if p.FirstName=null then 0 else p.FirstName.Length)
+ (if p.MiddleName=null then 0 else p.MiddleName.Length)
+ (if p.LastName=null then 0 else p.LastName.Length)
oder vielleicht
let TotalNumCharsInPersonsName(p:Person) =
p.FirstName.Length
+ (if p.MiddleName=null then 0 else p.MiddleName.Length)
+ p.LastName.Length
Angenommen, dies p
stellt sicher , dass zuerst / zuletzt vorhanden ist, aber die Mitte kann null sein, oder Sie führen Überprüfungen durch, die verschiedene Arten von Ausnahmen auslösen, oder wer weiß was. All diese verrückten Implementierungsoptionen und Dinge, über die man nachdenken sollte, tauchen auf, weil es diesen dummen darstellbaren Wert gibt, den man nicht will oder braucht.
Null fügt normalerweise unnötige Komplexität hinzu. Komplexität ist der Feind aller Software, und Sie sollten sich bemühen, die Komplexität zu reduzieren, wann immer dies sinnvoll ist.
(Beachten Sie auch, dass selbst diese einfachen Beispiele komplexer sind. Auch wenn a FirstName
nicht sein kann null
, string
kann a ""
(die leere Zeichenfolge) darstellen, was wahrscheinlich auch kein Personenname ist, den wir modellieren möchten. Bei nullbaren Zeichenfolgen kann es dennoch vorkommen, dass wir "bedeutungslose Werte darstellen". Auch hier können Sie dies entweder über Invarianten und bedingten Code zur Laufzeit oder mithilfe des Typsystems (z. B. um einen NonEmptyString
Typ zu haben ) bekämpfen Letzteres ist vielleicht schlecht beraten ("gute" Typen werden oft über eine Reihe gemeinsamer Operationen "geschlossen" und z. B. NonEmptyString
nicht geschlossen.SubString(0,0)
), aber es zeigt mehr Punkte im Designraum. Letztendlich gibt es in einem bestimmten Typsystem eine gewisse Komplexität, die sehr gut beseitigt werden kann, und eine andere Komplexität, die nur an sich schwieriger zu beseitigen ist. Der Schlüssel für dieses Thema ist, dass in fast jedem Typsystem die Änderung von "standardmäßig nullfähige Referenzen" zu "standardmäßig nicht nullfähige Referenzen" fast immer eine einfache Änderung ist, die das Typensystem im Kampf gegen Komplexität und erheblich verbessert bestimmte Arten von Fehlern und bedeutungslosen Zuständen ausschließen. Es ist also ziemlich verrückt, dass so viele Sprachen diesen Fehler immer wieder wiederholen.)