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)
programming-languages
functional-programming
null
nullpointerexception
non-nullable
Roman A. Taycher
źródło
źródło
Odpowiedzi:
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
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ćwtedy mógłbym zdefiniować
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 trzyPoint
s, co sami mająX
iY
wartości, ale niestetyPoint
s lubTriangle
może sama być ten szalony wartość zerową, że nie ma sensu do wykresów domeny pracuję w. ItdJeś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 AFirstName
i ALastName
, ale tylko niektórzy mająMiddleName
S, to chciałbym powiedzieć coś takiegogdzie
string
tutaj zakłada się, że jest to typ niedozwolony. Nie ma wtedy trudnych do ustalenia niezmienników i nieoczekiwanychNullReferenceException
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ę rachunkiemFirstName
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ę:
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
w końcu tworzysz takie rzeczy jak
który wysadza się w powietrze, jeśli przychodzący obiekt Person nie ma niezmiennika, że wszystko jest niepuste, lub
albo może
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
, tostring
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.)źródło
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:
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.
źródło
Wszystkie dotychczasowe odpowiedzi koncentrują się na tym, dlaczego
null
jest 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
Option
lubMaybe
reprezentują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
na kod uwzględniający opcje
nie ma dużej różnicy! Ale to także okropny sposób korzystania z Opcji ... To podejście jest o wiele czystsze:
Lub nawet:
Kiedy zaczniesz zajmować się Listą opcji, będzie jeszcze lepiej. Wyobraź sobie, że sama lista
people
jest opcjonalna:Jak to działa?
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ąć.
źródło
flatMap
byłby nazywany(>>=)
operatorem „wiązania” dla monad. Zgadza się, Haskellery lubią pingowaćflatMap
rzeczy tak bardzo, że umieszczamy je w logo naszego języka.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)Ponieważ ludzie wydają się go brakować:
null
jest 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:
null
przypadek na krawędzi.null = null
?nan = nan
?inf = inf
?+0 = -0
?+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:id_a
istnieje 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 rzeczyassert((bool)actionSheet + (bool)alertView == 1)
.źródło
assert(actionSheet ^ alertView)
? A może Twój język XOR nie może się chlubić?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:
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 #:
Jednak w języku takim jak Java bez bezpośredniej obsługi typów opcji mielibyśmy coś takiego:
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.źródło
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:
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ć:
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:
Powiedzmy teraz, że chcemy wydrukować nazwiska wszystkich kierowców wraz z ich numerami prawa jazdy.
W C:
W OCaml byłoby to:
Jak widać w tym trywialnym przykładzie, w bezpiecznej wersji nie ma nic skomplikowanego:
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.
źródło
NULL
jak 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).Microsoft Research ma ciekawy projekt o nazwie
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.
źródło
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:
Zobacz to pytanie także u programistów
źródło
Robert Nystrom oferuje fajny artykuł tutaj:
http://journal.stuffwithstuff.com/2010/08/23/void-null-maybe-and-nothing/
opisujący swój proces myślowy podczas dodawania wsparcia dla nieobecności i niepowodzenia w swoim języku programowania Sroka .
źródło
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ą.
źródło
Języki wektorowe mogą czasem uciec od braku wartości zerowej.
W tym przypadku pusty wektor służy jako wpisany pusty.
źródło