Myślę, że zwięzłe podsumowanie, dlaczego zero jest niepożądane, polega na tym, że stany pozbawione znaczenia nie powinny być reprezentowalne .
Załóżmy, że modeluję drzwi. Może być w jednym z trzech stanów: otwarty, zamknięty, ale odblokowany oraz zamknięty i zablokowany. Teraz mogłem go wymodelować według wzoru
class Door
private bool isShut
private bool isLocked
i jasne jest, jak zamapować moje trzy stany na te dwie zmienne logiczne. Ale pozostawia to czwarty stan niepożądany dostępny: isShut==false && isLocked==true
. Ponieważ typy, które wybrałem jako moją reprezentację, dopuszczają ten stan, muszę poświęcić wysiłek umysłowy, aby upewnić się, że klasa nigdy nie wejdzie w ten stan (być może poprzez jawne kodowanie niezmiennika). Natomiast gdybym używał języka z algebraicznymi typami danych lub sprawdzonymi wyliczeniami, które pozwalają mi zdefiniować
type DoorState =
| Open | ShutAndUnlocked | ShutAndLocked
wtedy mógłbym zdefiniować
class Door
private DoorState state
i nie ma już zmartwień. System typów zapewni, że wystąpią tylko trzy możliwe stany class Door
. Właśnie w tym rodzaju systemy są dobre - wyraźnie wykluczając całą klasę błędów w czasie kompilacji.
Problem null
polega na tym, że każdy typ odwołania otrzymuje ten dodatkowy stan w swojej przestrzeni, który zwykle jest niepożądany. string
Zmienna może być dowolny ciąg znaków, czy może to być ten szalony dodatkową null
wartość, która nie mapuje do mojej domeny problemu. Triangle
Obiekt ma trzy Point
s, co sami mają X
i Y
wartości, ale niestety Point
s lub Triangle
może sama być ten szalony wartość zerową, że nie ma sensu do wykresów domeny pracuję w. Itd
Jeśli zamierzasz modelować wartość, która może nie istnieć, powinieneś wyraźnie się na nią zdecydować. Jeśli zamierzam modelować ludzi, że każdy Person
ma A FirstName
i A LastName
, ale tylko niektórzy mają MiddleName
S, to chciałbym powiedzieć coś takiego
class Person
private string FirstName
private Option<string> MiddleName
private string LastName
gdzie string
tutaj zakłada się, że jest to typ niedozwolony. Nie ma wtedy trudnych do ustalenia niezmienników i nieoczekiwanych NullReferenceException
s przy próbie obliczenia długości czyjegoś imienia. System typów zapewnia, że każdy kod zajmujący się MiddleName
rachunkami ma taką możliwość None
, podczas gdy każdy kod zajmujący się rachunkiem FirstName
może bezpiecznie założyć, że istnieje tam wartość.
Na przykład, używając powyższego typu, moglibyśmy napisać tę głupią funkcję:
let TotalNumCharsInPersonsName(p:Person) =
let middleLen = match p.MiddleName with
| None -> 0
| Some(s) -> s.Length
p.FirstName.Length + middleLen + p.LastName.Length
bez obaw. Natomiast w języku z odwołaniami zerowalnymi dla typów takich jak łańcuch znaków, a następnie przy założeniu
class Person
private string FirstName
private string MiddleName
private string LastName
w końcu tworzysz takie rzeczy jak
let TotalNumCharsInPersonsName(p:Person) =
p.FirstName.Length + p.MiddleName.Length + p.LastName.Length
który wysadza się w powietrze, jeśli przychodzący obiekt Person nie ma niezmiennika, że wszystko jest niepuste, lub
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)
albo może
let TotalNumCharsInPersonsName(p:Person) =
p.FirstName.Length
+ (if p.MiddleName=null then 0 else p.MiddleName.Length)
+ p.LastName.Length
zakładając, że p
zapewnia, że są tam pierwsze / ostatnie, ale środek może być zerowy, lub może wykonujesz kontrole, które generują różne rodzaje wyjątków, lub kto wie, co. Wszystkie te szalone opcje implementacji i rzeczy do przemyślenia na temat pojawiania się, ponieważ istnieje ta głupia reprezentowalna wartość, której nie chcesz ani nie potrzebujesz.
Null zazwyczaj dodaje niepotrzebnej złożoności. Złożoność jest wrogiem wszelkiego oprogramowania i powinieneś starać się ją zmniejszać, gdy tylko jest to uzasadnione.
(Należy zauważyć, że nawet te proste przykłady są bardziej skomplikowane. Nawet jeśli FirstName
nie może być null
, to string
może reprezentować ""
(pusty ciąg), co prawdopodobnie nie jest również imieniem osoby, którą zamierzamy modelować. Jako taki, nawet jeśli nie jest ciągi dopuszczające wartości zerowe, nadal może się zdarzyć, że „reprezentujemy bezsensowne wartości”. Ponownie, możesz walczyć z tym albo za pomocą niezmienników i kodu warunkowego w czasie wykonywania, albo za pomocą systemu typów (np. mieć NonEmptyString
typ). to ostatnie jest być może niezrozumiałe („dobre” typy są często „zamykane” na zestaw typowych operacji, i np. NonEmptyString
nie są zamykane na.SubString(0,0)
), ale pokazuje więcej punktów w przestrzeni projektowej. Na koniec dnia w dowolnym systemie typów istnieje pewna złożoność, której pozbycie się będzie bardzo dobra, oraz inna złożoność, której z natury trudniej jest się pozbyć. Kluczem do tego tematu jest to, że w prawie każdym systemie typów zmiana z „domyślnie zerowalnych referencji” na „domyślnie zerowalnych referencji” jest prawie zawsze prostą zmianą, która sprawia, że system typów jest znacznie lepszy w walce ze złożonością i wykluczając pewne rodzaje błędów i bezsensownych stanów. Jest więc dość szalone, że tak wiele języków ciągle powtarza ten błąd.)