Najlepsze wyjaśnienie dla języków bez wartości null

225

Co jakiś czas, gdy programiści narzekają na zerowe błędy / wyjątki, ktoś pyta, co robimy bez wartości zerowej.

Mam podstawowe pojęcie o fajności typów opcji, ale nie mam wiedzy ani umiejętności językowych, aby najlepiej to wyrazić. Jakie jest świetne wytłumaczenie poniższych słów napisane w sposób przystępny dla przeciętnego programisty, na który możemy wskazać tę osobę?

  • Domyślnie nie ma potrzeby posiadania referencji / wskaźników
  • Jak działają typy opcji, w tym strategie ułatwiające sprawdzanie przypadków zerowych, takich jak
    • dopasowywanie wzorów i
    • rozumienia monadyczne
  • Alternatywne rozwiązanie, takie jak zero jedzenia wiadomości
  • (inne aspekty, za którymi tęskniłem)
Roman A. Taycher
źródło
11
Jeśli dodasz tagi do tego pytania w celu programowania funkcjonalnego lub F #, z pewnością otrzymasz fantastyczne odpowiedzi.
Stephen Swensen
Dodałem funkcjonalny tag programowania, ponieważ typ opcji pochodzi ze świata ml. Wolałbym nie oznaczać F # (zbyt konkretny). BTW ktoś z uprawnieniami taksonomii musi dodać tagi typu może lub typu.
Roman A. Taycher
4
Podejrzewam, że takie konkretne tagi nie są potrzebne. Tagi mają przede wszystkim umożliwić ludziom znalezienie odpowiednich pytań (na przykład „pytania, o których dużo wiem i będę w stanie odpowiedzieć”, a „programowanie funkcjonalne” jest tam bardzo pomocne. Ale coś w rodzaju „null” lub „ „type-type” są znacznie mniej przydatne. Niewiele osób może monitorować tag „type-type” w poszukiwaniu pytań, na które mogą odpowiedzieć;)
lipiec
Nie zapominajmy, że jednym z głównych powodów zerowania jest to, że komputery ewoluowały silnie powiązane z teorią zbiorów. Null jest jednym z najważniejszych zbiorów w całej teorii zbiorów. Bez tego wszystkie algorytmy uległyby awarii. Na przykład - wykonaj sortowanie scalające. Polega to na kilkukrotnym podzieleniu listy na pół. Co jeśli lista ma 7 pozycji? Najpierw podzielisz go na 4 i 3. Następnie 2, 2, 2 i 1. Następnie 1, 1, 1, 1, 1, 1, 1 i ... null! Null ma cel, tylko taki, którego praktycznie nie widzisz. Istnieje więcej dla dziedziny teoretycznej.
stevendesu
6
@steven_desu - Nie zgadzam się. W językach „zerowalnych” możesz mieć odwołanie do pustej listy [], a także odwołanie do pustej listy. To pytanie dotyczy zamieszania między nimi.
stusmith,

Odpowiedzi:

433

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 nullpolega na tym, że każdy typ odwołania otrzymuje ten dodatkowy stan w swojej przestrzeni, który zwykle jest niepożądany. stringZmienna może być dowolny ciąg znaków, czy może to być ten szalony dodatkową nullwartość, która nie mapuje do mojej domeny problemu. TriangleObiekt ma trzy Points, co sami mają Xi Ywartości, ale niestety Points lub Trianglemoż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 Personma A FirstNamei A LastName, ale tylko niektórzy mają MiddleNameS, to chciałbym powiedzieć coś takiego

class Person
    private string FirstName
    private Option<string> MiddleName
    private string LastName

gdzie stringtutaj zakłada się, że jest to typ niedozwolony. Nie ma wtedy trudnych do ustalenia niezmienników i nieoczekiwanych NullReferenceExceptions przy próbie obliczenia długości czyjegoś imienia. System typów zapewnia, że ​​każdy kod zajmujący się MiddleNamerachunkami ma taką możliwość None, podczas gdy każdy kod zajmujący się rachunkiem FirstNamemoż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 pzapewnia, ż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 FirstNamenie może być null, to stringmoż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ć NonEmptyStringtyp). to ostatnie jest być może niezrozumiałe („dobre” typy są często „zamykane” na zestaw typowych operacji, i np. NonEmptyStringnie 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.)

Brian
źródło
31
Re: names - Rzeczywiście. A może zależy ci na zamodelowaniu drzwi, które wiszą otwarte, ale z wystającym ryglem zamka, uniemożliwiającym zamknięcie drzwi. Na świecie jest dużo złożoności. Kluczem nie jest zwiększenie złożoności podczas implementacji mapowania między „stanami świata” a „stanami programu” w oprogramowaniu.
Brian
59
Co, nigdy nie zamknąłeś drzwi na klucz?
Joshua
58
Nie rozumiem, dlaczego ludzie pracują nad semantyką określonej domeny. Brian w zwięzły i prosty sposób przedstawił wady z wartością zero, tak, uprościł problematyczną dziedzinę w swoim przykładzie, mówiąc, że każdy ma imię i nazwisko. Odpowiedzi na pytanie udzielił „T”, Brian - jeśli kiedykolwiek jesteś w Bostonie, jestem ci winien piwo za wszystkie zamieszczone tutaj posty!
akaphenom
67
@akaphenom: dziękuję, ale zauważ, że nie wszyscy ludzie piją piwo (jestem osobą nie pijącą). Ale doceniam to, że używasz jedynie uproszczonego modelu świata, aby przekazać wdzięczność, więc nie będę więcej sprzeczał się z błędnymi założeniami twojego modelu świata. : P (Tyle złożoności w prawdziwym świecie! :))
Brian
4
O dziwo, na tym świecie są drzwi 3-państwowe! Są używane w niektórych hotelach jako drzwi do toalety. Przycisk działa jak klucz od wewnątrz, który zamyka drzwi od zewnątrz. Jest automatycznie odblokowywany, gdy tylko zasuwa się zasuwa.
comonad
65

Zaletą typów opcji nie jest to, że są one opcjonalne. Chodzi o to, że wszystkie inne typy nie są .

Czasami musimy być w stanie reprezentować rodzaj „zerowego” stanu. Czasami musimy reprezentować opcję „bez wartości”, a także inne możliwe wartości, które może przyjąć zmienna. Tak więc język, który całkowicie wyklucza, będzie nieco okaleczony.

Ale często tego nie potrzebujemy, a dopuszczenie takiego stanu „zerowego” prowadzi tylko do dwuznaczności i zamieszania: za każdym razem, gdy uzyskuję dostęp do zmiennej typu referencyjnego w .NET, muszę wziąć pod uwagę, że może to być null .

Często tak naprawdę nigdy nie będzie zerowy, ponieważ programista konstruuje kod tak, aby nigdy nie mógł się zdarzyć. Ale kompilator nie może tego zweryfikować i za każdym razem, gdy go widzisz, musisz zadać sobie pytanie: „czy to może być zerowe? Czy muszę tutaj sprawdzać, czy jest puste?”

Idealnie, w wielu przypadkach, w których null nie ma sensu, nie powinno być dozwolone .

Trudno to osiągnąć w .NET, gdzie prawie wszystko może być zerowe. Musisz polegać na autorze kodu, do którego dzwonisz, aby być w 100% zdyscyplinowanym i konsekwentnym oraz jasno udokumentować, co może, a czego nie może być zerowe, albo musisz być paranoikiem i sprawdzać wszystko .

Jeśli jednak typy nie są domyślnie zerowalne , nie musisz sprawdzać, czy są zerowe. Wiesz, że nigdy nie mogą być zerowe, ponieważ kompilator / moduł sprawdzania typów wymusza to za Ciebie.

A potem po prostu trzeba drzwi z powrotem dla tych rzadkich przypadkach, gdy mamy zrobić trzeba obsłużyć stan zerowy. Następnie można użyć typu „opcja”. Następnie zezwalamy na zero w przypadkach, w których podjęliśmy świadomą decyzję, że musimy być w stanie reprezentować przypadek „bez wartości”, aw każdym innym przypadku wiemy, że wartość nigdy nie będzie zerowa.

Jak wspomnieli inni, na przykład w języku C # lub Javie, null może oznaczać jedną z dwóch rzeczy:

  1. zmienna jest niezainicjowana. Najlepiej, aby nigdy tak się nie stało. Zmienna nie powinna istnieć, chyba że zostanie zainicjowana.
  2. zmienna zawiera pewne „opcjonalne” dane: musi być w stanie reprezentować przypadek, w którym nie ma danych . Czasami jest to konieczne. Być może próbujesz znaleźć obiekt na liście i nie wiesz z góry, czy on istnieje. Następnie musimy być w stanie przedstawić, że „nie znaleziono obiektu”.

Drugie znaczenie należy zachować, ale pierwsze należy całkowicie wyeliminować. I nawet drugie znaczenie nie powinno być domyślnym. Możemy zdecydować, czy i kiedy będziemy tego potrzebować . Ale kiedy nie potrzebujemy, aby coś było opcjonalne, chcemy, aby moduł sprawdzania typu gwarantował , że nigdy nie będzie zerowy.

jalf
źródło
A w drugim znaczeniu chcemy, aby kompilator nas ostrzegał (zatrzymać?), Jeśli spróbujemy uzyskać dostęp do takich zmiennych bez uprzedniego sprawdzenia nieważności. Oto świetny artykuł na temat nadchodzącej null / non-null C # (wreszcie!) Blogs.msdn.microsoft.com/dotnet/2017/11/15/…
Ohad Schneider
44

Wszystkie dotychczasowe odpowiedzi koncentrują się na tym, dlaczego nulljest zła rzecz i jak to jest przydatne, jeśli język może zagwarantować, że pewne wartości nigdy nie będą zerowe.

Następnie sugerują, że dobrym pomysłem byłoby wymuszenie braku wartości dla wszystkich wartości, co można zrobić, jeśli dodasz koncepcję podobną do Optionlub Maybereprezentującą typy, które nie zawsze mają określoną wartość. Takie podejście podjął Haskell.

To wszystko dobre rzeczy! Ale nie wyklucza to użycia typów o zerowym / zerowym typie, aby osiągnąć ten sam efekt. Dlaczego zatem Option jest nadal dobrą rzeczą? W końcu Scala obsługuje wartości zerowalne ( musi , więc może współpracować z bibliotekami Java), ale także obsługuje Options.

P: Więc jakie są korzyści poza całkowitym usunięciem wartości zerowych z języka?

A. Skład

Jeśli dokonasz naiwnego tłumaczenia z kodu zerowego

def fullNameLength(p:Person) = {
  val middleLen =
    if (null == p.middleName)
      p.middleName.length
    else
      0
  p.firstName.length + middleLen + p.lastName.length
}

na kod uwzględniający opcje

def fullNameLength(p:Person) = {
  val middleLen = p.middleName match {
    case Some(x) => x.length
    case _ => 0
  }
  p.firstName.length + middleLen + p.lastName.length
}

nie ma dużej różnicy! Ale to także okropny sposób korzystania z Opcji ... To podejście jest o wiele czystsze:

def fullNameLength(p:Person) = {
  val middleLen = p.middleName map {_.length} getOrElse 0
  p.firstName.length + middleLen + p.lastName.length
}

Lub nawet:

def fullNameLength(p:Person) =       
  p.firstName.length +
  p.middleName.map{length}.getOrElse(0) +
  p.lastName.length

Kiedy zaczniesz zajmować się Listą opcji, będzie jeszcze lepiej. Wyobraź sobie, że sama lista peoplejest opcjonalna:

people flatMap(_ find (_.firstName == "joe")) map (fullNameLength)

Jak to działa?

//convert an Option[List[Person]] to an Option[S]
//where the function f takes a List[Person] and returns an S
people map f

//find a person named "Joe" in a List[Person].
//returns Some[Person], or None if "Joe" isn't in the list
validPeopleList find (_.firstName == "joe")

//returns None if people is None
//Some(None) if people is valid but doesn't contain Joe
//Some[Some[Person]] if Joe is found
people map (_ find (_.firstName == "joe")) 

//flatten it to return None if people is None or Joe isn't found
//Some[Person] if Joe is found
people flatMap (_ find (_.firstName == "joe")) 

//return Some(length) if the list isn't None and Joe is found
//otherwise return None
people flatMap (_ find (_.firstName == "joe")) map (fullNameLength)

Odpowiedni kod z zerowymi czekami (a nawet operatorami Elvis?:) Byłby boleśnie długi. Prawdziwą sztuczką jest tutaj operacja flatMap, która pozwala na zagnieżdżone rozumienie Opcji i kolekcji w sposób, którego wartości zerowe nigdy nie są w stanie osiągnąć.

Kevin Wright
źródło
8
+1, jest to dobry punkt do podkreślenia. Jeden dodatek: w Haskell-land flatMapbyłby nazywany (>>=)operatorem „wiązania” dla monad. Zgadza się, Haskellery lubią pingować flatMaprzeczy tak bardzo, że umieszczamy je w logo naszego języka.
CA McCann
1
+1 Mam nadzieję, że wyrażenie Option<T>nigdy, nigdy nie będzie zerowe. Niestety, Scala jest nadal związana z Javą :-) (Z drugiej strony, gdyby Scala nie grała dobrze z Javą, kto by z niej korzystał? Oo)
Łatwo to zrobić: „List (null) .headOption”. Zauważ, że oznacza to coś zupełnie innego niż zwracana wartość „None”
Kevin Wright
4
Dałem ci nagrodę, ponieważ bardzo podoba mi się to, co powiedziałeś o kompozycji, o czym inni nie wspominali.
Roman A. Taycher
Doskonała odpowiedź ze świetnymi przykładami!
thSoft
38

Ponieważ ludzie wydają się go brakować: nulljest niejednoznaczny.

Alice ma datę urodzenia null. Co to znaczy?

Data śmierci Boba to null . Co to znaczy?

„Rozsądną” interpretacją może być to, że data urodzenia Alicji istnieje, ale jest nieznana, podczas gdy data śmierci Boba nie istnieje (Bob wciąż żyje). Ale dlaczego doszliśmy do różnych odpowiedzi?


Kolejny problem: nullprzypadek na krawędzi.

  • Jest null = null ?
  • Jest nan = nan ?
  • Jest inf = inf ?
  • Jest +0 = -0 ?
  • Jest +0/0 = -0/0 ?

Odpowiedzi są zwykle odpowiednio „tak”, „nie”, „tak”, „tak”, „nie”, „tak”. Szaleni „matematycy” nazywają NaN „nieważnością” i mówią, że porównuje się do siebie. SQL traktuje wartości zerowe jako nic nie równe (więc zachowują się jak NaN). Można się zastanawiać, co się stanie, gdy spróbujesz przechowywać ± ∞, ± 0 i NaN w tej samej kolumnie bazy danych (są 2 53 NaN, z których połowa jest „ujemna”).

Co gorsza, bazy danych różnią się sposobem traktowania wartości NULL, a większość z nich nie jest spójna (zobacz Obsługa wartości NULL w SQLite ). To jest okropne.


A teraz obowiązkowa historia:

Niedawno zaprojektowałem tabelę bazy danych (sqlite3) z pięcioma kolumnami a NOT NULL, b, id_a, id_b NOT NULL, timestamp. Ponieważ jest to ogólny schemat zaprojektowany w celu rozwiązania ogólnego problemu dla dość dowolnych aplikacji, istnieją dwa ograniczenia wyjątkowości:

UNIQUE(a, b, id_a)
UNIQUE(a, b, id_b)

id_aistnieje tylko dla kompatybilności z istniejącym projektem aplikacji (częściowo dlatego, że nie wymyśliłem lepszego rozwiązania) i nie jest używany w nowej aplikacji. Ze względu na sposób, w jaki NULL działa w SQL, mogę wstawiać (1, 2, NULL, 3, t)i(1, 2, NULL, 4, t) nie naruszają pierwszy unikatowości (bo (1, 2, NULL) != (1, 2, NULL)).

Działa to szczególnie ze względu na to, jak NULL działa w ograniczeniu wyjątkowości w większości baz danych (przypuszczalnie dzięki temu łatwiej jest modelować sytuacje „w świecie rzeczywistym”, np. Żadna osoba nie może mieć tego samego numeru ubezpieczenia społecznego, ale nie wszyscy mają taki numer).


FWIW, bez uprzedniego wywołania niezdefiniowanego zachowania, referencje C ++ nie mogą „wskazywać” na null i nie jest możliwe zbudowanie klasy z niezainicjowanymi zmiennymi elementów referencyjnych (jeśli zgłoszony zostanie wyjątek, konstrukcja się nie powiedzie).

Sidenote: Czasami możesz chcieć wzajemnie się wykluczających wskaźników (tzn. Tylko jeden z nich może nie mieć wartości NULL), np. W hipotetycznym iOS type DialogState = NotShown | ShowingActionSheet UIActionSheet | ShowingAlertView UIAlertView | Dismissed. Zamiast tego jestem zmuszony robić takie rzeczy assert((bool)actionSheet + (bool)alertView == 1).

tc.
źródło
Rzeczywistych matematyków nie używamy jednak pojęcia „NaN”, zapewniamy.
Noldorin,
@Noldorin: Tak, ale używają terminu „forma nieokreślona”.
IJ Kennedy
@IJKennedy: To inna uczelnia, którą znam całkiem dobrze, dziękuję. Niektóre NaN mogą reprezentować nieokreśloną formę, ale ponieważ FPA nie robi rozumowania symbolicznego, utożsamianie go z nieokreśloną formą jest dość mylące!
Noldorin
Co jest nie tak z assert(actionSheet ^ alertView)? A może Twój język XOR nie może się chlubić?
kot
16

Domyślnie nie ma potrzeby posiadania referencji / wskaźników.

Nie sądzę, że jest to główny problem z zerami, głównym problemem z zerami jest to, że mogą oznaczać dwie rzeczy:

  1. Odwołanie / wskaźnik jest niezainicjowany: problem tutaj jest taki sam jak zmienność w ogóle. Po pierwsze utrudnia analizę kodu.
  2. Zmienna o wartości null w rzeczywistości coś znaczy: właśnie tak formalizują się typy Opcji.

Języki, które obsługują typy Opcji, zazwyczaj również zabraniają lub zniechęcają do używania niezainicjowanych zmiennych.

Jak działają typy opcji, w tym strategie ułatwiające sprawdzanie przypadków zerowych, takie jak dopasowanie wzorca.

Aby były skuteczne, typy Opcji muszą być obsługiwane bezpośrednio w języku. W przeciwnym razie potrzeba dużo kodu płyty kotła, aby je zasymulować. Dopasowywanie wzorców i wnioskowanie o typach to dwie kluczowe funkcje językowe, dzięki którym typy Opcji są łatwe w obsłudze. Na przykład:

W F #:

//first we create the option list, and then filter out all None Option types and 
//map all Some Option types to their values.  See how type-inference shines.
let optionList = [Some(1); Some(2); None; Some(3); None]
optionList |> List.choose id //evaluates to [1;2;3]

//here is a simple pattern-matching example
//which prints "1;2;None;3;None;".
//notice how value is extracted from op during the match
optionList 
|> List.iter (function Some(value) -> printf "%i;" value | None -> printf "None;")

Jednak w języku takim jak Java bez bezpośredniej obsługi typów opcji mielibyśmy coś takiego:

//here we perform the same filter/map operation as in the F# example.
List<Option<Integer>> optionList = Arrays.asList(new Some<Integer>(1),new Some<Integer>(2),new None<Integer>(),new Some<Integer>(3),new None<Integer>());
List<Integer> filteredList = new ArrayList<Integer>();
for(Option<Integer> op : list)
    if(op instanceof Some)
        filteredList.add(((Some<Integer>)op).getValue());

Alternatywne rozwiązanie, takie jak zero jedzenia wiadomości

„Wiadomość zjadła zero” w Objective-C jest nie tyle rozwiązaniem, ile próbą złagodzenia bólu związanego z kontrolą zerową. Zasadniczo zamiast rzucać wyjątek czasu wykonywania podczas próby wywołania metody na obiekcie o wartości NULL, wyrażenie wyraża wartość samego NULL. Zawieszając niedowierzanie, to tak, jakby każda instancja zaczynała się od if (this == null) return null;. Ale wtedy dochodzi do utraty informacji: nie wiesz, czy metoda zwróciła wartość NULL, ponieważ jest to poprawna wartość zwracana, lub ponieważ obiekt jest w rzeczywistości NULL. Jest to podobne do połykania wyjątków i nie czyni żadnego postępu w rozwiązywaniu problemów z zerowym przedstawionym wcześniej.

Stephen Swensen
źródło
To jest wkurzony, ale język c # nie jest językiem podobnym do języka c.
Roman A. Taycher
4
Szukałem tutaj Javy, ponieważ C # prawdopodobnie miałby lepsze rozwiązanie ... ale doceniam twoją wulgaryzmy, co ludzie naprawdę mają na myśli, to „język ze składnią inspirowaną c”. Poszedłem dalej i zastąpiłem stwierdzenie „c”.
Stephen Swensen
Z linq, prawda. Myślałem o c # i nie zauważyłem tego.
Roman A. Taycher
1
Tak, głównie ze składnią inspirowaną c, ale myślę, że słyszałem także o imperatywnych językach programowania, takich jak python / ruby, z bardzo małą ilością składni podobnej do c, którą funkcjonalni programiści nazywają c-podobną.
Roman A. Taycher
11

Zgromadzenie przyniosło nam adresy znane również jako nietypowe wskaźniki. C zmapował je bezpośrednio jako wskaźniki maszynowe, ale wprowadził null Algola jako unikalną wartość wskaźnika, kompatybilną ze wszystkimi wskaźnikami maszynowymi. Dużym problemem z zerowym w C jest to, że ponieważ każdy wskaźnik może być pusty, nigdy nie można bezpiecznie używać wskaźnika bez ręcznego sprawdzania.

W językach wyższego poziomu zerowanie jest niewygodne, ponieważ naprawdę przekazuje dwa różne pojęcia:

  • Mówienie, że coś jest niezdefiniowane .
  • Mówienie, że coś jest opcjonalne .

Posiadanie niezdefiniowanych zmiennych jest prawie bezużyteczne i powoduje niezdefiniowane zachowanie, ilekroć wystąpią. Przypuszczam, że wszyscy zgodzą się, że za wszelką cenę należy unikać niezdefiniowanych rzeczy.

Drugi przypadek jest opcjonalny i najlepiej podać go wyraźnie, na przykład z typem opcji .


Załóżmy, że jesteśmy firmą transportową i musimy stworzyć aplikację, która pomoże stworzyć harmonogram dla naszych kierowców. Dla każdego kierowcy przechowujemy kilka informacji, takich jak: posiadane prawa jazdy i numer telefonu, pod który można zadzwonić w razie nagłego wypadku.

W C moglibyśmy mieć:

struct PhoneNumber { ... };
struct MotorbikeLicence { ... };
struct CarLicence { ... };
struct TruckLicence { ... };

struct Driver {
  char name[32]; /* Null terminated */
  struct PhoneNumber * emergency_phone_number;
  struct MotorbikeLicence * motorbike_licence;
  struct CarLicence * car_licence;
  struct TruckLicence * truck_licence;
};

Jak zauważysz, przy przetwarzaniu naszej listy sterowników będziemy musieli sprawdzić zerowe wskaźniki. Kompilator ci nie pomoże, bezpieczeństwo programu zależy od twoich ramion.

W OCaml ten sam kod wyglądałby tak:

type phone_number = { ... }
type motorbike_licence = { ... }
type car_licence = { ... }
type truck_licence = { ... }

type driver = {
  name: string;
  emergency_phone_number: phone_number option;
  motorbike_licence: motorbike_licence option;
  car_licence: car_licence option;
  truck_licence: truck_licence option;
}

Powiedzmy teraz, że chcemy wydrukować nazwiska wszystkich kierowców wraz z ich numerami prawa jazdy.

W C:

#include <stdio.h>

void print_driver_with_truck_licence_number(struct Driver * driver) {
  /* Check may be redundant but better be safe than sorry */
  if (driver != NULL) {
    printf("driver %s has ", driver->name);
    if (driver->truck_licence != NULL) {
      printf("truck licence %04d-%04d-%08d\n",
        driver->truck_licence->area_code
        driver->truck_licence->year
        driver->truck_licence->num_in_year);
    } else {
      printf("no truck licence\n");
    }
  }
}

void print_drivers_with_truck_licence_numbers(struct Driver ** drivers, int nb) {
  if (drivers != NULL && nb >= 0) {
    int i;
    for (i = 0; i < nb; ++i) {
      struct Driver * driver = drivers[i];
      if (driver) {
        print_driver_with_truck_licence_number(driver);
      } else {
        /* Huh ? We got a null inside the array, meaning it probably got
           corrupt somehow, what do we do ? Ignore ? Assert ? */
      }
    }
  } else {
    /* Caller provided us with erroneous input, what do we do ?
       Ignore ? Assert ? */
  }
}

W OCaml byłoby to:

open Printf

(* Here we are guaranteed to have a driver instance *)
let print_driver_with_truck_licence_number driver =
  printf "driver %s has " driver.name;
  match driver.truck_licence with
    | None ->
        printf "no truck licence\n"
    | Some licence ->
        (* Here we are guaranteed to have a licence *)
        printf "truck licence %04d-%04d-%08d\n"
          licence.area_code
          licence.year
          licence.num_in_year

(* Here we are guaranteed to have a valid list of drivers *)
let print_drivers_with_truck_licence_numbers drivers =
  List.iter print_driver_with_truck_licence_number drivers

Jak widać w tym trywialnym przykładzie, w bezpiecznej wersji nie ma nic skomplikowanego:

  • To jest terser.
  • Otrzymasz znacznie lepsze gwarancje i nie jest wymagana żadna kontrola zerowa.
  • Kompilator zapewnił, że poprawnie wykonałeś tę opcję

Podczas gdy w C mogłeś po prostu zapomnieć o zerowym czeku i bumie ...

Uwaga: te przykłady kodu nie zostały skompilowane, ale mam nadzieję, że masz pomysły.

bltxd
źródło
Nigdy tego nie próbowałem, ale en.wikipedia.org/wiki/Cyclone_%28programming_language%29 twierdzi, że zezwala na wskaźniki inne niż c dla c.
Roman A. Taycher
1
Nie zgadzam się z twoim stwierdzeniem, że nikt nie jest zainteresowany pierwszą sprawą. Wiele osób, szczególnie tych z funkcjonalnych społeczności językowych, jest bardzo tym zainteresowanych i albo zniechęca, albo całkowicie zabrania używania niezainicjowanych zmiennych.
Stephen Swensen
Wydaje mi się, że NULLjak napisano „odniesienie, które może nie wskazywać na nic” dla niektórych języków Algolu (Wikipedia zgadza się, patrz en.wikipedia.org/wiki/Null_pointer#Null_pointer ). Ale oczywiście jest prawdopodobne, że programiści asemblerzy zainicjowali swoje wskaźniki na niepoprawny adres (czytaj: Null = 0).
1
@Stephen: Prawdopodobnie mieliśmy na myśli to samo. Dla mnie zniechęcają lub zabraniają używania niezainicjowanych rzeczy właśnie dlatego, że nie ma sensu dyskutować o niezdefiniowanych rzeczach, ponieważ nie możemy zrobić z nimi nic zdrowego lub pożytecznego. Nie miałoby to żadnego interesu.
bltxd
2
jako @tc. mówi, że null nie ma nic wspólnego z montażem. W montaż typu są na ogół nie pustych. Wartość załadowana do rejestru ogólnego przeznaczenia może wynosić zero lub może być niezerową liczbą całkowitą. Ale nigdy nie może być zerowy. Nawet jeśli załadujesz adres pamięci do rejestru, w większości popularnych architektur nie ma osobnej reprezentacji „wskaźnika zerowego”. To koncepcja wprowadzona w językach wyższego poziomu, takich jak C.
czerwiec
5

Microsoft Research ma ciekawy projekt o nazwie

Spec #

Jest to rozszerzenie C # z niepustym typem i pewnym mechanizmem sprawdzania, czy obiekty nie są zerowe , chociaż IMHO, stosując zasadę projektowania na podstawie umowy, może być bardziej odpowiednie i bardziej pomocne w wielu kłopotliwych sytuacjach spowodowanych odwołaniami zerowymi.

Jahan
źródło
4

Pochodząc z .NET, zawsze myślałem, że null ma sens, jest użyteczny. Dopóki nie poznałem struktur i tego, jak łatwo z nimi pracowałem, unikając mnóstwa kodu bojlera. Tony Hoare przemawiając w QCon London w 2009 roku, przeprosił za wymyślenie zerowej referencji . Cytując go:

Nazywam to moim błędem za miliard dolarów. Był to wynalazek referencji zerowej w 1965 roku. W tym czasie projektowałem pierwszy kompleksowy system typów dla referencji w języku obiektowym (ALGOL W). Moim celem było upewnienie się, że każde użycie referencji powinno być całkowicie bezpieczne, a sprawdzanie wykonywane automatycznie przez kompilator. Ale nie mogłem oprzeć się pokusie wprowadzenia zerowej referencji, po prostu dlatego, że tak łatwo ją wdrożyć. Doprowadziło to do niezliczonych błędów, podatności i awarii systemu, które prawdopodobnie spowodowały miliard dolarów bólu i szkód w ciągu ostatnich czterdziestu lat. W ostatnich latach wiele analizatorów programów, takich jak PREfix i PREfast w Microsoft, było używanych do sprawdzania referencji i ostrzegania, jeśli istnieje ryzyko, że mogą one nie mieć wartości zerowej. Nowsze języki programowania, takie jak Spec #, wprowadziły deklaracje dla odwołań niepustych. To jest rozwiązanie, które odrzuciłem w 1965 roku.

Zobacz to pytanie także u programistów

nawfal
źródło
1

Zawsze patrzyłem na Null (lub zero) jako brak wartości .

Czasami tego chcesz, czasem nie. To zależy od domeny, z którą pracujesz. Jeśli nieobecność jest znacząca: bez drugiego imienia, wówczas Twoja aplikacja może działać odpowiednio. Z drugiej strony, jeśli wartość null nie powinna tam być: Imię to null, wówczas programista otrzymuje przysłowiową rozmowę telefoniczną o 2 nad ranem.

Widziałem też, że kod jest przeciążony i nadmiernie skomplikowany w sprawdzaniu wartości null. Dla mnie oznacza to jedną z dwóch rzeczy:
a) błąd wyżej w drzewie aplikacji
b) zły / niepełny projekt

Z drugiej strony - Null jest prawdopodobnie jednym z bardziej użytecznych pojęć do sprawdzania, czy coś jest nieobecne, a języki bez pojęcia null doprowadzą do nadmiernej komplikacji, gdy nadejdzie czas na sprawdzenie poprawności danych. W takim przypadku, jeśli nowa zmienna nie zostanie zainicjowana, wspomniane języki zwykle ustawiają zmienne na pusty ciąg, 0 lub pustą kolekcję. Jednak jeśli pusty ciąg lub 0 lub pusta kolekcja są poprawnymi wartościami dla Twojej aplikacji - masz problem.

Czasami jest to obchodzone przez wymyślanie specjalnych / dziwnych wartości dla pól, które reprezentują niezainicjowany stan. Ale co się stanie, gdy użytkownik wprowadzi specjalną wartość? I nie wdawajmy się w bałagan, który spowoduje procedury sprawdzania poprawności danych. Jeśli język wspiera koncepcję zerową, wszystkie obawy znikną.

Jon
źródło
Cześć @Jon, Trochę cię tu śledzę. W końcu zdałem sobie sprawę, że przez „specjalne / dziwne” wartości prawdopodobnie masz na myśli coś takiego jak „niezdefiniowany” JavaScript lub „NaN” IEEE. Ale poza tym tak naprawdę nie odnosisz się do żadnego z pytań zadanych przez PO. A stwierdzenie, że „Null jest prawdopodobnie najbardziej użytecznym pojęciem do sprawdzania, czy czegoś nie ma” jest prawie na pewno błędne. Typy opcji są dobrze przemyślaną i bezpieczną alternatywą dla typu null.
Stephen Swensen
@ Stephen - Właściwie patrząc wstecz na moją wiadomość, myślę, że cała druga połowa powinna zostać przeniesiona na pytanie, które należy zadać. Ale nadal mówię, że null jest bardzo przydatny do sprawdzania, czy coś jest nieobecne.
Jon
0

Języki wektorowe mogą czasem uciec od braku wartości zerowej.

W tym przypadku pusty wektor służy jako wpisany pusty.

Jozuego
źródło
Myślę, że rozumiem o czym mówisz, ale czy mógłbyś wymienić kilka przykładów? Zwłaszcza zastosowania wielu funkcji do możliwie zerowej wartości?
Roman A. Taycher
Dobrze zastosowanie transformacji wektorowej do pustego wektora skutkuje kolejnym pustym wektorem. Do Twojej wiadomości, SQL jest głównie językiem wektorowym.
Joshua
1
OK, lepiej to wyjaśnię. SQL to język wektorowy dla wierszy i język wartości dla kolumn.
Joshua