Czy zerowe referencje są naprawdę złe?

161

Słyszałem, że powiedzenie, że włączenie zerowych referencji w językach programowania jest „błędem miliarda dolarów”. Ale dlaczego? Jasne, mogą powodować wyjątki NullReference, ale co z tego? Każdy element języka może być źródłem błędów, jeśli zostanie użyty nieprawidłowo.

A jaka jest alternatywa? Przypuszczam, że zamiast powiedzieć to:

Customer c = Customer.GetByLastName("Goodman"); // returns null if not found
if (c != null)
{
    Console.WriteLine(c.FirstName + " " + c.LastName + " is awesome!");
}
else { Console.WriteLine("There was no customer named Goodman.  How lame!"); }

Możesz to powiedzieć:

if (Customer.ExistsWithLastName("Goodman"))
{
    Customer c = Customer.GetByLastName("Goodman") // throws error if not found
    Console.WriteLine(c.FirstName + " " + c.LastName + " is awesome!"); 
}
else { Console.WriteLine("There was no customer named Goodman.  How lame!"); }

Ale jak to jest lepsze? Tak czy inaczej, jeśli zapomnisz sprawdzić, czy klient istnieje, otrzymasz wyjątek.

Podejrzewam, że wyjątek CustomerNotFoundException jest nieco łatwiejszy do debugowania niż wyjątek NullReferenceException, ponieważ jest bardziej opisowy. Czy to wszystko?

Tim Goodman
źródło
54
Byłbym ostrożny z podejściem „jeśli klient istnieje / zdobądź klienta”, gdy tylko napiszesz dwa wiersze takiego kodu, otworzysz drzwi do warunków wyścigu. Pobierz, a następnie sprawdź, czy wyjątki zerowe / połowowe są bezpieczniejsze.
Carson63000,
26
Gdybyśmy tylko mogli wyeliminować źródło wszystkich błędów w czasie wykonywania, nasz kod byłby doskonały! Jeśli odniesienia NULL w C # psują mózg, spróbuj użyć niepoprawnych, ale nie NULL, wskaźników w C;)
LnxPrgr3
jednak w niektórych językach istnieją zerowe operacje koalescencji i bezpiecznej nawigacji
jk.
35
null per se nie jest zły. System typów, który nie robi różnicy między typem T a typem T + null, jest zły.
Ingo
27
Wracając do mojego pytania kilka lat później, całkowicie zmieniłem podejście Option / Może.
Tim Goodman

Odpowiedzi:

91

zero jest złe

Na stronie InfoQ znajduje się prezentacja na ten temat: Null References: The Billion Dollar Mistake autorstwa Tony Hoare

Rodzaj opcji

Alternatywą dla programowania funkcjonalnego jest użycie typu opcji , który może zawierać SOME valuelub NONE.

Dobry artykuł Wzorzec „Opcji” omawiający typ Opcji i zapewniający jego implementację w Javie.

Znalazłem także raport o błędzie dla Javy dotyczący tego problemu: dodaj do Javy ładne typy Opcji, aby zapobiec wyjątkom NullPointerExceptions . Zwróciła funkcja została wprowadzona w Java 8.

Jonas
źródło
5
Nullable <x> w C # jest podobną koncepcją, ale daleko mu do implementacji wzorca opcji. Głównym powodem jest to, że w .NET System.Nullable jest ograniczony do typów wartości.
MattDavey,
27
+1 Dla typu opcji. Po zapoznaniu się z być może Haskell , wartości zerowe zaczynają wyglądać dziwnie ...
Andres F.,
5
Deweloper może popełnić błąd w dowolnym miejscu, więc zamiast wyjątku zerowego wskaźnika otrzymasz pusty wyjątek opcji. Jak ten drugi jest lepszy od pierwszego?
greenoldman
7
@greenoldman nie ma czegoś takiego jak „wyjątek pustej opcji”. Spróbuj wstawić wartości NULL do kolumn bazy danych zdefiniowanych jako NOT NULL.
Jonas
36
@greenoldman Problem z zerami polega na tym, że wszystko może być zerowe, a jako programista musisz być bardzo ostrożny. StringTyp nie jest tak naprawdę ciągiem. To String or Nulltyp. Języki, które w pełni przestrzegają idei typów opcji, nie dopuszczają wartości pustych w swoim systemie typów, co daje gwarancję, że jeśli zdefiniujesz podpis typu, który wymaga String(lub cokolwiek innego), musi on mieć wartość. OptionTyp jest tam dać reprezentację poziomu typu rzeczy, które mogą lub nie mogą mieć jakąś wartość, więc wiesz, gdzie trzeba jawnie obsługiwać te sytuacje.
KChaloux
127

Problem polega na tym, że ponieważ teoretycznie każdy obiekt może być zerowy i podrzuca wyjątek, gdy próbujesz go użyć, twój kod obiektowy jest w zasadzie zbiorem niewybuchów.

Masz rację, że płynna obsługa błędów może być funkcjonalnie identyczna z ifinstrukcjami sprawdzającymi wartość zerową . Ale co się dzieje, gdy coś, co sam siebie przekonałeś, nie może być zerowe, jest w rzeczywistości zerowe? Kerboom. Cokolwiek się wydarzy, jestem gotów się założyć, że 1) to nie będzie pełne wdzięku i 2) nie spodoba ci się.

I nie odrzucaj wartości „łatwego debugowania”. Dojrzały kod produkcji to szalone, rozłożyste stworzenie; wszystko, co daje lepszy wgląd w to, co poszło nie tak i gdzie może zaoszczędzić wiele godzin kopania.

BlairHippo
źródło
27
+1 Zazwyczaj błędy w kodzie produkcyjnym POTRZEBUJĄ być zidentyfikowane tak szybko, jak to możliwe, aby można je było naprawić (zwykle błąd danych). Im łatwiej jest debugować, tym lepiej.
4
Ta odpowiedź jest taka sama, jeśli zostanie zastosowana do jednego z przykładów PO. Albo testujesz pod kątem błędów, albo nie, i albo sobie z nimi radzisz z wdziękiem, albo nie. Innymi słowy, usunięcie pustych referencji z języka nie zmieni klas błędów, które wystąpią, po prostu subtelnie zmieni ich manifestację.
dash-tom-bang
19
+1 dla niewybuchów. Przy zerowych referencjach powiedzenie public Foo doStuff()nie oznacza „rób rzeczy i zwróć wynik Foo”, oznacza „rób rzeczy i zwróć wynik Foo, LUB zwróć null, ale nie masz pojęcia, czy tak się stanie, czy nie”. W rezultacie musisz sprawdzić WSZĘDZIE, aby mieć pewność. Równie dobrze można usunąć wartości null i użyć specjalnego typu lub wzorca (np. Typu wartości), aby wskazać wartość bez znaczenia.
Matt Olenik,
12
@greenoldman, twój przykład jest podwójnie skuteczny, aby zademonstrować nasz punkt widzenia, że ​​stosowanie typów pierwotnych (zerowych lub całkowitych) jako modeli semantycznych jest złym postępowaniem. Jeśli masz typ, dla którego liczby ujemne nie są prawidłową odpowiedzią, powinieneś utworzyć nową klasę typów, aby zaimplementować model semantyczny o takim znaczeniu. Teraz cały kod do obsługi modelowania tego nieujemnego typu jest zlokalizowany w swojej klasie, odizolowany od reszty systemu, a każda próba utworzenia wartości ujemnej może zostać natychmiast przechwycona.
Huperniketes
9
@greenoldman, nadal udowadniasz, że prymitywne typy są nieodpowiednie do modelowania. Najpierw było ponad liczbami ujemnymi. Teraz jest ponad operatorem podziału, gdy zero jest dzielnikiem. Jeśli wiesz, że nie możesz podzielić przez zero, możesz przewidzieć, że wynik nie będzie ładny, i jesteś odpowiedzialny za upewnienie się, że testujesz, oraz podanie odpowiedniej „prawidłowej” wartości dla tego przypadku. Twórcy oprogramowania są odpowiedzialni za znajomość parametrów, w których działa ich kod i odpowiednie kodowanie. To właśnie odróżnia inżynierię od majsterkowania.
Huperniketes
50

Istnieje kilka problemów z użyciem zerowych odwołań w kodzie.

Po pierwsze, jest zwykle używany do wskazania specjalnego stanu . Zamiast definiowania nowej klasy lub stałej dla każdego stanu, gdy specjalizacje są zwykle wykonywane, użycie zerowego odniesienia oznacza użycie stratnego, masowo uogólnionego typu / wartości .

Po drugie, debugowanie kodu staje się trudniejsze, gdy pojawia się odwołanie zerowe i próbujesz ustalić, co go wygenerowało , który stan obowiązuje i jego przyczynę, nawet jeśli możesz prześledzić jego ścieżkę wykonania.

Po trzecie, odniesienia zerowe wprowadzają dodatkowe ścieżki kodu do testowania .

Po czwarte, gdy odniesienia zerowe są używane jako poprawne stany parametrów, a także wartości zwracane, programowanie obronne (dla stanów spowodowanych przez projekt) wymaga więcej sprawdzania zerowych odniesień w różnych miejscach … na wszelki wypadek.

Po piąte, środowisko wykonawcze języka już wykonuje sprawdzanie typu, gdy wykonuje wyszukiwanie selektora w tabeli metod obiektu. Powielasz więc wysiłek, sprawdzając, czy typ obiektu jest poprawny / nieprawidłowy, a następnie sprawdzając, czy środowisko wykonawcze jest poprawne typu obiektu, aby wywołać jego metodę.

Dlaczego nie skorzystać z wzorca NullObject, aby skorzystać z kontroli środowiska wykonawczego, aby wywołać metody NOP specyficzne dla tego stanu (zgodne z interfejsem zwykłego stanu), a jednocześnie wyeliminować wszystkie dodatkowe sprawdzanie odwołań zerowych w całej bazie kodu?

To wymaga więcej pracy , tworząc klasę NullObject dla każdego interfejsu, z którym chcesz reprezentować szczególny stan. Ale przynajmniej specjalizacja jest izolowana dla każdego stanu specjalnego, a nie kodu, w którym stan może być obecny. IOW liczba testów jest zmniejszona, ponieważ masz mniej alternatywnych ścieżek wykonania w swoich metodach.

Huperniketes
źródło
7
... ponieważ ustalenie, kto wygenerował (a raczej nie zadał sobie trudu zmiany lub zainicjowania) danych śmieci wypełniających rzekomo użyteczny obiekt, jest o wiele łatwiejsze niż śledzenie odwołania NULL? Nie kupuję tego, chociaż utknąłem głównie w ziemi o typie statycznym.
dash-tom-bang
Dane śmieci są jednak o wiele rzadsze niż odwołania zerowe. Szczególnie w zarządzanych językach.
Dean Harding,
5
-1 dla obiektu zerowego.
DeadMG,
4
Punkty (4) i (5) są pełne, ale obiekt NullObject nie jest lekarstwem. Wpadniesz w jeszcze gorszą pułapkę - błędy logiczne (workflow). Gdy znasz DOKŁADNIE obecną sytuację, na pewno może to pomóc, ale używanie jej na ślepo (zamiast zerowej) będzie cię kosztować więcej.
greenoldman
1
@ gnasher729, podczas gdy środowisko wykonawcze Objective-C nie zgłasza wyjątku zerowego wskaźnika, tak jak robią to Java i inne systemy, nie poprawia to używania odwołań zerowych z powodów opisanych w mojej odpowiedzi. Na przykład, jeśli w [self.navigationController.presentingViewController dismissViewControllerAnimated: YES completion: nil];środowisku wykonawczym nie ma nawigacjiController traktuje go jako sekwencję braku operacji, widok nie jest usuwany z wyświetlacza i możesz siedzieć przed Xcode, drapiąc się po głowie, godzinami próbując dowiedzieć się, dlaczego.
Huperniketes
48

Wartości zerowe nie są takie złe, chyba że się ich nie spodziewasz. Państwo powinno trzeba wyraźnie określić w kodzie, który czekasz null, który jest problem języka projektowania. Rozważ to:

Customer? c = Customer.GetByLastName("Goodman");
// note the question mark -- 'c' is either a Customer or null
if (c != null)
{
    // c is not null, therefore its type in this block is
    // now 'Customer' instead of 'Customer?'
    Console.WriteLine(c.FirstName + " " + c.LastName + " is awesome!");
}
else { Console.WriteLine("There was no customer named Goodman.  How lame!"); }

Jeśli spróbujesz wywołać metody na a Customer?, powinieneś otrzymać błąd czasu kompilacji. Powodem, dla którego więcej języków tego nie robi (IMO), jest to, że nie przewidują zmiany typu zmiennej w zależności od zakresu, w jakim się znajduje. Jeśli język sobie z tym poradzi, problem można rozwiązać w całości w system typów.

Istnieje również funkcjonalny sposób radzenia sobie z tym problemem, przy użyciu typów opcji i Maybe, ale nie jestem tak dobrze z nim zaznajomiony. Wolę tak, ponieważ poprawny kod powinien teoretycznie wymagać tylko jednego znaku dodanego do poprawnej kompilacji.

Uwaga, aby pomyśleć o nazwie
źródło
11
Wow, to całkiem fajne. Oprócz wychwytywania znacznie większej liczby błędów w czasie kompilacji, oszczędza mi to konieczności sprawdzania dokumentacji, aby dowiedzieć się, czy GetByLastNamezwraca null, jeśli nie zostanie znaleziony (w przeciwieństwie do zgłaszania wyjątku) - po prostu widzę, czy zwraca Customer?lub Customer.
Tim Goodman
14
@JBRWilkinson: To tylko gotowy przykład, aby pokazać pomysł, a nie przykład rzeczywistej implementacji. Myślę, że to całkiem niezły pomysł.
Dean Harding
1
Masz rację, że nie jest to wzorzec zerowy.
Dean Harding
13
Wygląda to tak, jak Haskell nazywa Maybe- A Maybe Customerto albo Just c(gdzie cjest a Customer) albo Nothing. W innych językach nazywa się to Option- zobacz niektóre inne odpowiedzi na to pytanie.
MatrixFrog
2
@SimonBarker: ty możesz być pewien, czy Customer.FirstNamejest typu String(w przeciwieństwie do String?). To jest prawdziwa korzyść - tylko zmienne / właściwości, które mają znaczącą wartość nic, powinny być zadeklarowane jako zerowalne.
Yogu
20

Istnieje wiele doskonałych odpowiedzi, które obejmują niefortunne objawy null, dlatego chciałbym przedstawić alternatywny argument: Null jest wadą w systemie typów.

System typów ma na celu zapewnienie, że różne komponenty programu „pasują do siebie” prawidłowo; dobrze napisany program nie może „zejść z szyn” w niezdefiniowane zachowanie.

Zastanów się nad hipotetycznym dialektem Java lub jakimkolwiek preferowanym językiem o typie statycznym, w którym możesz przypisać ciąg "Hello, world!"do dowolnej zmiennej dowolnego typu:

Foo foo1 = new Foo();  // Legal
Foo foo2 = "Hello, world!"; // Also legal
Foo foo3 = "Bonjour!"; // Not legal - only "Hello, world!" is allowed

I możesz sprawdzić takie zmienne:

if (foo1 != "Hello, world!") {
    bar(foo1);
} else {
    baz();
}

Nie ma w tym nic niemożliwego - ktoś mógłby zaprojektować taki język, gdyby chciał. Szczególna wartość nie musi być "Hello, world!"- może to już liczba 42, krotka (1, 4, 9), lub, powiedzmy null. Ale dlaczego miałbyś to zrobić? Zmienna typu Foopowinna zawierać tylko Foos - to jest cały punkt systemu typów! nullnie jest Fooniczym więcej niż "Hello, world!"jest. Co gorsza, nullnie jest wartością dowolnego typu, a nic nie można z tym zrobić!

Programista nigdy nie może mieć pewności, że zmienna rzeczywiście zawiera zmienną Foo, a program też nie; Aby uniknąć niezdefiniowanego zachowania, musi sprawdzić zmienne "Hello, world!"przed użyciem ich jako Foos. Zauważ, że sprawdzanie ciągu w poprzednim fragmencie nie propaguje faktu, że foo1 jest naprawdę Foo- barprawdopodobnie również będzie miał własne sprawdzanie, dla bezpieczeństwa.

Porównaj to z użyciem typu Maybe/ Optionz dopasowaniem wzorca:

case maybeFoo of
 |  Just foo => bar(foo)
 |  Nothing => baz()

Wewnątrz Just fooklauzuli zarówno Ty, jak i program na pewno wiesz, że nasza Maybe Foozmienna naprawdę zawiera Foowartość - ta informacja jest propagowana w dół łańcucha wywołań i barnie musi wykonywać żadnych kontroli. Ponieważ Maybe Foojest to odmienny typ od Foo, jesteś zmuszony poradzić sobie z możliwością, że może on zawierać Nothing, więc nigdy nie możesz być zaskoczony przez NullPointerException. Możesz znacznie łatwiej zrozumieć swój program, a kompilator może pominąć sprawdzanie wartości NULL, wiedząc, że wszystkie zmienne typu Foonaprawdę zawierają Foos. Wszyscy wygrywają.

Doval
źródło
Co z wartościami zerowymi w Perlu 6? Nadal są zerowe, z wyjątkiem tego, że zawsze są wpisywane. Czy nadal możesz napisać problem my Int $variable = Int, gdzie Intjest pusta wartość typu Int?
Konrad Borowski
@xfix Nie znam Perla, ale dopóki nie masz możliwości egzekwowania this type only contains integersw przeciwieństwie do niego this type contains integers and this other value, będziesz mieć problem za każdym razem, gdy chcesz tylko liczb całkowitych.
Doval,
1
+1 za dopasowanie wzoru. Biorąc pod uwagę liczbę odpowiedzi powyżej, które wspominają o typach Opcji, można by pomyśleć, że ktoś wspomniałby o tym, że prosta funkcja języka, taka jak dopasowywanie wzorców, może sprawić, że będzie o wiele bardziej intuicyjna w obsłudze niż wartości zerowe.
Jules,
1
Myślę, że wiązanie monadyczne jest nawet bardziej przydatne niż dopasowanie wzorca (chociaż oczywiście wiązanie dla Może jest realizowane za pomocą dopasowania wzorca). Myślę, że to zabawne widzieć języki, takie jak C #, wprowadzające specjalną składnię dla wielu rzeczy ( ??, ?.itd.) Dla rzeczy, które języki takie jak haskell implementują jako funkcje biblioteczne. Myślę, że jest o wiele ładniejszy. null/ Nothingpropagation nie jest w żaden sposób „specjalny”, więc dlaczego powinniśmy nauczyć się więcej składni, aby go mieć?
sara
14

(Rzucam kapeluszem w ringu za stare pytanie;))

Specyficzny problem z wartością null polega na tym, że przerywa on pisanie statyczne.

Jeśli mam Thing t, to kompilator może zagwarantować, że zadzwonię t.doSomething(). Cóż, UNLESS tjest zerowy w czasie wykonywania. Teraz wszystkie zakłady są wyłączone. Kompilator powiedział, że jest OK, ale dowiaduję się później, że ttak naprawdę NIE doSomething(). Zamiast więc być w stanie zaufać kompilatorowi, aby wychwycił błędy typu, muszę poczekać, aż środowisko wykonawcze je wyłapie. Równie dobrze mogę po prostu użyć Pythona!

W pewnym sensie wartość null wprowadza dynamiczne pisanie do systemu o typie statycznym, z oczekiwanymi rezultatami.

Różnica między dzieleniem przez zero lub logiem ujemnym itp. Polega na tym, że gdy i = 0, to nadal jest liczbą całkowitą. Kompilator nadal może zagwarantować swój typ. Problem polega na tym, że logika błędnie stosuje tę wartość w sposób, który nie jest dozwolony przez logikę ... ale jeśli logika to robi, jest to w zasadzie definicja błędu.

(Kompilator może wychwycić niektóre z tych problemów, BTW. Jak oznaczanie wyrażeń jak i = 1 / 0w czasie kompilacji. Ale tak naprawdę nie można oczekiwać, że kompilator przejdzie do funkcji i zapewni, że wszystkie parametry są zgodne z logiką funkcji)

Problem praktyczny polega na tym, że wykonujesz dużo dodatkowej pracy i dodajesz zerowe kontrole, aby zabezpieczyć się w czasie wykonywania, ale co jeśli zapomnisz jeden? Kompilator powstrzymuje Cię przed przypisywaniem:

Ciąg s = nowa liczba całkowita (1234);

Dlaczego więc miałoby to umożliwiać przypisanie wartości (zerowej), która spowoduje przerwanie odwołań s?

Mieszając w kodzie „brak wartości” z „wpisanymi” referencjami, nakładasz dodatkowe obciążenie na programistów. A kiedy zdarzają się wyjątki NullPointer, śledzenie ich może być jeszcze bardziej czasochłonne. Zamiast polegać na statycznym pisaniu i powiedzieć „to jest odniesienie do czegoś oczekiwanego”, pozwalasz językowi powiedzieć „to może być odniesienie do czegoś oczekiwanego”.

Rob Y
źródło
3
W C zmienna typu *intidentyfikuje albo int, albo NULL. Podobnie w C ++, zmienna typu *Widgetalbo identyfikuje Widgetrzecz, której typ pochodzi Widgetlub NULL. Problem z Java i pochodne jak C # jest to, że używają oni miejsce przechowywania Widgetznaczyć *Widget. Rozwiązaniem nie jest udawanie, że *Widgetnie powinien być w stanie pomieścić NULL, ale raczej odpowiedni Widgettyp lokalizacji przechowywania.
supercat
14

nullWskaźnik jest narzędziem

nie wróg. Po prostu powinieneś ich użyć dobrze.

Pomyśl tylko chwilę, ile czasu zajmuje znalezienie i naprawienie typowego nieprawidłowego błędu wskaźnika , w porównaniu do zlokalizowania i naprawienia błędu wskaźnika zerowego. Łatwo jest sprawdzić wskaźnik na null. Jest to PITA weryfikująca, czy niepuste wskaźniki wskazują prawidłowe dane.

Jeśli nadal nie jesteś przekonany, dodaj odpowiednią dawkę wielowątkowości do swojego scenariusza, a następnie pomyśl jeszcze raz.

Morał tej historii: nie wylewaj dzieci z wodą. I nie obwiniaj narzędzi za wypadek. Człowiek, który wymyślił liczbę zero dawno temu, był całkiem sprytny, ponieważ tego dnia można było nazwać „nic”. nullWskaźnik nie jest tak daleko od tego.


EDYCJA: Chociaż NullObjectwzorzec wydaje się lepszym rozwiązaniem niż odwołania, które mogą być zerowe, sam wprowadza problemy:

  • Odwołanie przechowujące NullObjectpowinno (zgodnie z teorią) nic nie robić, gdy wywoływana jest metoda. W ten sposób można wprowadzić subtelne błędy z powodu niepoprawnie nieprzypisanych odwołań, które teraz są gwarantowane jako niepuste (yehaa!), Ale wykonują niepożądane działanie: nic. Z NPE jest prawie oczywiste, gdzie leży problem. W przypadku NullObjectzachowania, które zachowuje się jakoś (ale źle), zwiększamy ryzyko wykrycia błędów (zbyt) późno. Nie jest to przypadkiem podobne do problemu z nieprawidłowym wskaźnikiem i ma podobne konsekwencje: Odwołanie wskazuje na coś, co wygląda na prawidłowe dane, ale nie jest, wprowadza błędy danych i może być trudne do wyśledzenia. Szczerze mówiąc, w tych przypadkach wolałbym bez mrugnięcia okiem, który natychmiast zawiedzie ,przez błąd logiczny / danych, który nagle uderza mnie później. Pamiętasz badanie IBM o koszcie błędów będącym funkcją, na którym etapie są one wykrywane?

  • Pojęcie nie robienia niczego, gdy wywoływana jest metoda a NullObject, nie obowiązuje, gdy wywoływany jest getter właściwości lub funkcja, lub gdy metoda zwraca wartości przez out. Powiedzmy, że zwracana jest wartość inti decydujemy, że „nic nie rób” oznacza „powrót 0”. Ale skąd możemy mieć pewność, że 0 jest właściwym „niczym” do zwrotu (nie)? W końcu NullObjectnic nie powinno robić, ale kiedy zostanie poproszony o wartość, musi jakoś zareagować. Niepowodzenie nie jest opcją: używamy, NullObjectaby zapobiec NPE, i na pewno nie wymienimy go na inny wyjątek (niewdrożony, dzielimy przez zero, ...), prawda? Jak więc poprawnie nic nie zwracać, gdy trzeba coś zwrócić?

Nie mogę nic NullObjectna to poradzić , ale kiedy ktoś próbuje zastosować wzór do każdego problemu, wygląda to bardziej jak próba naprawy jednego błędu przez wykonanie drugiego. Jest to bez wątpienia dobre i przydatne rozwiązanie w niektórych przypadkach, ale z pewnością nie jest magiczną kulą we wszystkich przypadkach.

JensG
źródło
Czy możesz przedstawić argument za tym, dlaczego nullpowinien istnieć? Fala ręczna jest możliwa niemal z każdą wadą językową za pomocą „to nie jest problem, po prostu nie popełnij błędu”.
Doval
Do jakiej wartości ustawiłbyś nieprawidłowy wskaźnik?
JensG
Cóż, nie ustawisz ich na żadną wartość, ponieważ w ogóle nie powinieneś na to pozwolić. Zamiast tego używasz zmiennej innego typu, być może nazywanej, Maybe Tktóra może zawierać albo wartość Nothing, albo Twartość. Kluczową kwestią jest to, że musisz sprawdzić, czy Maybenaprawdę zawiera jakiś element, Tzanim będziesz mógł go używać, i podać sposób działania, na wypadek, gdyby tak nie było. A ponieważ zabroniłeś nieprawidłowych wskaźników, teraz nigdy nie musisz sprawdzać niczego, co nie jest Maybe.
Doval
1
@JensG w ostatnim przykładzie, czy kompilator zmusza Cię do sprawdzenia wartości zerowej przed każdym wywołaniem t.Something()? Czy może ma jakiś sposób wiedzieć, że już wykonałeś czek, i nie zmuszać Cię do powtórzenia? Co jeśli zrobiłeś czek przed przejściem tna tę metodę? Dzięki typowi opcji kompilator wie, czy sprawdziłeś już, czy nie, patrząc na typ t. Jeśli jest to opcja, jej nie zaznaczyłeś, a jeśli jest to wartość nieopakowana, to masz.
Tim Goodman
1
Co zwraca obiekt zerowy, gdy zostaniesz zapytany o wartość?
JensG
8

Odwołania zerowe są błędem, ponieważ pozwalają na kod nie sensowny:

foo = null
foo.bar()

Istnieją alternatywy, jeśli wykorzystasz system typów:

Maybe<Foo> foo = null
foo.bar() // error{Maybe<Foo> does not have any bar method}

Ogólnie rzecz biorąc, pomysł polega na umieszczeniu zmiennej w pudełku, a jedyną rzeczą, którą możesz zrobić, jest jej rozpakowanie, najlepiej skorzystanie z pomocy kompilatora, jak zaproponowano dla Eiffela.

Haskell ma go od zera ( Maybe), w C ++ możesz wykorzystać, boost::optional<T>ale nadal możesz uzyskać niezdefiniowane zachowanie ...

Matthieu M.
źródło
6
-1 dla „dźwigni”!
Mark C
8
Ale z pewnością wiele popularnych konstrukcji programistycznych dopuszcza nonsensowny kod, nie? Dzielenie przez zero jest (prawdopodobnie) nonsensowne, ale nadal pozwalamy na dzielenie i mamy nadzieję, że programista pamięta, aby sprawdzić, czy dzielnik nie jest zerowy (lub obsłużyć wyjątek).
Tim Goodman
3
Nie jestem pewien co masz na myśli przez „od zera”, ale Maybenie jest wbudowana w języku rdzenia, jego definicja właśnie dzieje się w standardowym preludium: data Maybe t = Nothing | Just t.
fredoverflow 18.10.10
4
@FredOverflow: ponieważ preludium jest ładowane domyślnie, uważam, że „od zera”, Haskell ma zadziwiająco wystarczający system typowania, aby umożliwić zdefiniowanie (i innych) drobiazgów z ogólnymi regułami języka to tylko bonus :)
Matthieu M.,
2
@TimGoodman Podczas gdy dzielenie przez zero może nie być najlepszym przykładem dla wszystkich typów wartości liczbowych (IEEE 754 definiuje wynik dzielenia przez zero), w przypadkach, w których spowodowałoby wyjątek, zamiast Tdzielenia wartości typu przez inne wartości typu T, ograniczyłbyś operację do wartości typu Tpodzielonych przez wartości typu NonZero<T>.
kbolino
8

A jaka jest alternatywa?

Opcjonalne typy i dopasowanie wzorca. Ponieważ nie znam C #, oto fragment kodu w fikcyjnym języku o nazwie Scala # :-)

Customer.GetByLastName("Goodman")    // returns Option[Customer]
match
{
    case Some(customer) =>
    Console.WriteLine(customer.FirstName + " " + customer.LastName + " is awesome!");

    case None =>
    Console.WriteLine("There was no customer named Goodman.  How lame!");
}
fredoverflow
źródło
6

Problem z zerami polega na tym, że języki, które pozwalają im prawie zmusić cię do programowania w obronie przed nim. Potrzeba dużo wysiłku (znacznie więcej niż próba użycia defensywnych bloków if), aby się upewnić

  1. przedmioty, których nie oczekujesz, że są zerowe, w rzeczywistości nigdy nie są zerowe, i
  2. że twoje mechanizmy obronne faktycznie radzą sobie ze wszystkimi potencjalnymi NPE.

Rzeczywiście, wartości zerowe są kosztowne.

luis.espinal
źródło
1
Pomocne może być podanie przykładu, w którym radzenie sobie z zerami zajmuje dużo więcej wysiłku. W moim, co prawda zbyt uproszczonym przykładzie, wysiłek jest mniej więcej taki sam. Nie zgadzam się z tobą, po prostu znajduję konkretne (pseudo) przykłady kodu, które dają wyraźniejszy obraz niż ogólne stwierdzenia.
Tim Goodman
2
Ach, nie wyjaśniłem się jasno. Nie jest tak, że trudniej jest radzić sobie z zerami. Jest to niezwykle trudne (jeśli nie niemożliwe), aby zagwarantować, że cały dostęp do potencjalnie zerowych referencji jest już bezpiecznie chroniony. Oznacza to, że w języku, który dopuszcza zerowe odwołania, po prostu niemożliwe jest zagwarantowanie, że fragment kodu o dowolnym rozmiarze jest wolny od wyjątków wskaźnika zerowego, nie bez pewnych poważnych trudności i wysiłków. Właśnie dlatego referencje zerowe są kosztowne. Niezwykle drogie jest zagwarantowanie całkowitego braku wyjątków zerowego wskaźnika.
luis.espinal
4

Problem, w jakim stopniu Twój język programowania próbuje udowodnić poprawność programu przed jego uruchomieniem. W języku wpisywanym statycznie udowadniasz, że masz poprawne typy. Przechodząc do domyślnych odwołań niepozwalających na zerowanie (z opcjonalnymi odwołaniami dopuszczającymi wartości zerowe) możesz wyeliminować wiele przypadków, w których przekazano wartość NULL i nie powinno tak być. Pytanie brzmi, czy dodatkowy wysiłek związany z obsługą odwołań, które nie są zerowalne, jest wart korzyści pod względem poprawności programu.

Winston Ewert
źródło
4

Problem nie jest tak bardzo zerowy, że w wielu współczesnych językach nie można określić typu odwołania innego niż zerowy.

Na przykład twój kod może wyglądać

public void MakeCake(Egg egg, Flour flour, Milk milk)
{
    if (egg == null) { throw ... }
    if (flour == null) { throw ... }
    if (milk == null) { throw ... }

    egg.Crack();
    MixingBowl.Mix(egg, flour, milk);
    // etc
}

// inside Mixing bowl class
public void Mix(Egg egg, Flour flour, Milk milk)
{
    if (egg == null) { throw ... }
    if (flour == null) { throw ... }
    if (milk == null) { throw ... }

    //... etc
}

Kiedy referencje klas są przekazywane, programowanie obronne zachęca do ponownego sprawdzenia wszystkich parametrów pod kątem wartości zerowej, nawet jeśli właśnie sprawdziłeś je pod kątem wartości zerowej bezpośrednio, szczególnie podczas budowania testowanych jednostek wielokrotnego użytku. To samo odniesienie można łatwo sprawdzić pod kątem wartości null 10 razy w całej bazie kodu!

Czy nie byłoby lepiej, gdybyś mógł mieć normalny typ zerowy, gdy dostajesz rzecz, radzić sobie z null wtedy i tam, a następnie przekazać go jako typ zerowy do wszystkich swoich funkcji pomocniczych i klas bez sprawdzania, czy wszystkie wartości są zerowe czas?

Taki jest sens rozwiązania Option - nie pozwala na włączanie stanów błędów, ale pozwala na projektowanie funkcji, które domyślnie nie akceptują argumentów zerowych. To, czy „domyślna” implementacja typów jest zerowalna, czy nie, jest mniej ważna niż elastyczność posiadania obu narzędzi.

http://twistedoakstudios.com/blog/Post330_non-nullable-types-vs-c-fixing-the-billion-dollar-mistake

Jest to również wymienione jako druga najczęściej głosowana funkcja w C # -> https://visualstudio.uservoice.com/forums/121579-visual-studio/category/30931-languages-c

Michael Parker
źródło
Myślę, że inną ważną kwestią dotyczącą Opcji <T> jest to, że jest to monada (a zatem także endofunkcja), dzięki czemu można komponować złożone obliczenia na strumieniach wartości zawierających typy opcjonalne bez wyraźnego sprawdzania Nonekażdego kroku.
sara
3

Null jest zły. Jednak brak wartości zerowej może być większym złem.

Problem polega na tym, że w prawdziwym świecie często masz sytuację, w której nie masz danych. Twój przykład wersji bez zer może wciąż wysadzić w powietrze - albo popełniłeś błąd logiczny i zapomniałeś sprawdzić Goodmana, albo być może Goodman ożenił się między tym, kiedy sprawdziłeś, a kiedy ją podniosłeś. (Pomaga w ocenie logiki, jeśli uważasz, że Moriarty ogląda każdy kawałek twojego kodu i próbuje cię zaskoczyć.)

Co robi wyszukiwanie Goodman, kiedy jej nie ma? Bez wartości zerowej musisz zwrócić jakiegoś domyślnego klienta - a teraz sprzedajesz rzeczy temu domyślnemu.

Zasadniczo sprowadza się to do tego, czy ważniejsze jest, aby kod działał bez względu na to, czy działa poprawnie. Jeśli niewłaściwe zachowanie jest lepsze niż brak zachowania, nie chcesz zer. (Jako przykład tego, jak może to być właściwy wybór, rozważ pierwsze uruchomienie Ariane V. Nieprzechwycony błąd / 0 spowodował, że rakieta wykonała ostry obrót, a samozniszczenie wystrzeliło z tego powodu. Wartość próbował obliczyć, że w rzeczywistości nie służył już żadnemu celowi od momentu włączenia wzmacniacza - wykonałby orbitę pomimo rutynowego zwracania śmieci.)

Przynajmniej 99 razy na 100 wolałbym używać wartości null. Jednak skoczyłbym z radości na notatkę ich własnej wersji.

Loren Pechtel
źródło
1
To dobra odpowiedź. Konstrukcja zerowa w programowaniu nie stanowi problemu, są to wszystkie wystąpienia wartości zerowych. Jeśli utworzysz obiekt domyślny, ostatecznie wszystkie wartości null zostaną zastąpione tymi wartościami domyślnymi, a interfejs użytkownika, DB i wszędzie pomiędzy nimi zostaną wypełnione tymi wartościami, które prawdopodobnie nie są już bardziej przydatne. Ale będziesz spać w nocy, bo chyba twój program się nie zawiesza.
Chris McCall,
2
Zakładasz, że nulljest to jedyny (a nawet preferowany) sposób modelowania braku wartości.
sara
1

Zoptymalizuj pod kątem najczęstszego przypadku.

Konieczność ciągłego sprawdzania wartości zerowej jest żmudna - chcesz mieć możliwość chwycenia obiektu klienta i pracy z nim.

W normalnym przypadku powinno to działać dobrze - sprawdzasz, pobierasz obiekt i go używasz.

W wyjątkowym przypadku , gdy (losowo) wyszukujesz klienta po imieniu, nie wiedząc, czy ten rekord / obiekt istnieje, potrzebujesz pewnej wskazówki, że to się nie udało. W tej sytuacji odpowiedzią jest zgłoszenie wyjątku RecordNotFound (lub pozwolić dostawcy SQL poniżej zrobić to za Ciebie).

Jeśli znajdujesz się w sytuacji, gdy nie wiesz, czy możesz ufać przychodzącym danym (parametrowi), być może dlatego, że zostały one wprowadzone przez użytkownika, możesz również podać „TryGetCustomer (nazwa, klient zewnętrzny)” wzór. Odsyłacz z int.Parse i int.TryParse.

JBRWilkinson
źródło
Zapewniam, że nigdy nie wiadomo, czy można ufać nadchodzącym danym, ponieważ przychodzą one do funkcji i są typem zerowalnym. Kod można by refaktoryzować, aby całkowicie zmienić dane wejściowe. Więc jeśli jest możliwe zerowanie, zawsze musisz sprawdzić. W efekcie powstaje system, w którym każda zmienna jest sprawdzana pod kątem wartości null za każdym razem przed uzyskaniem dostępu. Przypadki, w których nie jest zaznaczone, stają się procentem niepowodzenia dla twojego programu.
Chris McCall,
0

Wyjątki nie są ewidentne!

Są tam z jakiegoś powodu. Jeśli kod działa nieprawidłowo, istnieje genialny wzorzec zwany wyjątkiem , który informuje, że coś jest nie tak.

Unikając używania obiektów zerowych, ukrywasz część tych wyjątków. Nie przesadzam z przykładem OP, w którym konwertuje wyjątek zerowego wskaźnika na dobrze wpisany wyjątek, może to być dobra rzecz, ponieważ zwiększa czytelność. Jak zawsze, gdy trochę typu opcji rozwiązania jak @Jonas wskazał, ukrywasz wyjątki.

Niż w twojej aplikacji produkcyjnej, zamiast wyjątku, który zostanie zgłoszony po kliknięciu przycisku, aby wybrać pustą opcję, nic się nie dzieje. Zamiast pustego wskaźnika zostałby zgłoszony wyjątek i prawdopodobnie otrzymalibyśmy raport o tym wyjątku (jak w wielu aplikacjach produkcyjnych mamy taki mechanizm), a następnie moglibyśmy go naprawić.

Uczynienie kodu kuloodpornym poprzez unikanie wyjątku jest złym pomysłem, czyniąc go kodem kuloodpornym poprzez ustalenie wyjątku, cóż, to jest ścieżka, którą wybrałbym.

Ilya Gazman
źródło
1
Wiem teraz trochę więcej o typie Opcji niż kiedy opublikowałem pytanie kilka lat temu, ponieważ teraz używam ich regularnie w Scali. Chodzi o to, że opcja przypisuje Nonecoś, co nie jest Opcją, do błędu w czasie kompilacji, a także użycie Optiontak, jakby miała wartość bez uprzedniego sprawdzenia, jest błędem w czasie kompilacji. Błędy kompilacji są z pewnością lepsze niż wyjątki w czasie wykonywania.
Tim Goodman
Na przykład: Nie możesz powiedzieć s: String = None, musisz powiedzieć s: Option[String] = None. A jeśli sjest Opcją, nie możesz powiedzieć s.length, jednak możesz sprawdzić, czy ma ona wartość, i wyodrębnić ją w tym samym czasie, dopasowując wzór:s match { case Some(str) => str.length; case None => throw RuntimeException("Where's my value?") }
Tim Goodman
Niestety w Scali nadal można powiedzieć s: String = null, ale taka jest cena interoperacyjności z Javą. Zasadniczo zabrania się tego rodzaju rzeczy w naszym kodzie Scala.
Tim Goodman
2
Zgadzam się jednak z ogólnym komentarzem, że wyjątki (właściwie stosowane) nie są złe, a „naprawianie” wyjątku przez połykanie go jest ogólnie okropnym pomysłem. Ale w przypadku typu Option celem jest przekształcenie wyjątków w błędy czasu kompilacji, które są lepszą rzeczą.
Tim Goodman
@TimGoodman to taktyka tylko Scala, czy może być używana w innych językach, takich jak Java i ActionScript 3? Byłoby również miło, gdybyś mógł edytować swoją odpowiedź z pełnym przykładem.
Ilya Gazman
0

Nullssą problematyczne, ponieważ muszą zostać wyraźnie sprawdzone, ale kompilator nie jest w stanie ostrzec, że zapomniałeś je sprawdzić. Może to powiedzieć tylko czasochłonna analiza statyczna. Na szczęście istnieje kilka dobrych alternatyw.

Wyjmij zmienną z zakresu. Zbyt często nulljest używany jako symbol zastępczy, gdy programista deklaruje zmienną zbyt wcześnie lub utrzymuje ją zbyt długo. Najlepszym podejściem jest po prostu pozbycie się zmiennej. Nie deklaruj tego, dopóki nie będziesz mieć poprawnej wartości do wprowadzenia. To nie jest tak trudne ograniczenie, jak mogłoby się wydawać, i sprawia, że ​​kod jest o wiele czystszy.

Użyj wzorca zerowego obiektu. Czasami brakująca wartość jest poprawnym stanem dla twojego systemu. Dobrym rozwiązaniem jest tutaj wzorzec zerowy. Pozwala to uniknąć konieczności ciągłego jawnego sprawdzania, ale nadal umożliwia reprezentowanie stanu zerowego. Nie jest to „papering nad warunkiem błędu”, jak twierdzą niektórzy ludzie, ponieważ w tym przypadku stan zerowy jest prawidłowym stanem semantycznym. Biorąc to pod uwagę, nie należy używać tego wzorca, gdy stan zerowy nie jest prawidłowym stanem semantycznym. Powinieneś po prostu usunąć swoją zmienną z zakresu.

Użyj opcji / Może. Przede wszystkim pozwala to kompilatorowi ostrzec, że musisz sprawdzić brakującą wartość, ale nie tylko zastępuje jeden typ jawnego sprawdzania innym. Za pomocą Optionsmożesz połączyć swój kod w łańcuch i kontynuować, jakby twoja wartość istniała, bez konieczności sprawdzania aż do absolutnej ostatniej chwili. W Scali twój przykładowy kod wyglądałby mniej więcej tak:

val customer = customerDb getByLastName "Goodman"
val awesomeMessage =
  customer map (c => s"${c.firstName} ${c.lastName} is awesome!")
val notFoundMessage = "There was no customer named Goodman.  How lame!"
println(awesomeMessage getOrElse notFoundMessage)

W drugim wierszu, w którym budujemy niesamowitą wiadomość, nie dokonujemy wyraźnego sprawdzenia, czy klient został znaleziony, a piękno polega na tym , że nie musimy . Dopiero ostatnia linia, która może być wiele linii później, lub nawet w innym module, w którym wyraźnie zajmujemy się tym, co się stanie, jeśli tak Optionjest None. Mało tego, gdybyśmy o tym zapomnieli, nie sprawdziłby typu. Nawet wtedy odbywa się to w bardzo naturalny sposób, bez wyraźnego ifoświadczenia.

Porównaj to z opcją null, w której typ sprawdza się dobrze, ale gdzie musisz wyraźnie sprawdzić na każdym kroku lub cała aplikacja wysadza się w czasie wykonywania, jeśli masz szczęście w przypadku użycia, twoje testy jednostkowe ćwiczą. To po prostu nie jest warte kłopotów.

Karl Bielefeldt
źródło
0

Głównym problemem NULL jest to, że sprawia, że ​​system jest zawodny. W 1980 roku Tony Hoare w artykule poświęconym swojej Nagrody Turinga napisał:

Dlatego moja najlepsza rada dla pomysłodawców i projektantów ADA została zignorowana. … Nie należy zezwalać na używanie tego języka w jego obecnym stanie w aplikacjach, w których niezawodność ma kluczowe znaczenie, tj. W elektrowniach jądrowych, pociskach wycieczkowych, systemach wczesnego ostrzegania, antybalistycznych systemach obrony przeciwrakietowej. Następna rakieta, która zbłądzi w wyniku błędu języka programowania, może nie być rakietą kosmiczną podczas nieszkodliwej podróży na Wenus: może to być głowica nuklearna eksplodująca nad jednym z naszych miast. Niewiarygodny język programowania generujący niewiarygodne programy stanowi znacznie większe ryzyko dla naszego środowiska i społeczeństwa niż niebezpieczne samochody, toksyczne pestycydy lub wypadki w elektrowniach jądrowych. Bądź czujny, aby zmniejszyć ryzyko, a nie je zwiększyć.

Od tego czasu język ADA bardzo się zmienił, jednak takie problemy nadal występują w Javie, C # i wielu innych popularnych językach.

Obowiązkiem dewelopera jest tworzenie umów między klientem a dostawcą. Na przykład w języku C #, podobnie jak w Javie, można użyć Genericsdo zminimalizowania wpływu Nullodwołania, tworząc tylko do odczytu NullableClass<T>(dwie opcje):

class NullableClass<T>
{
     public HasValue {get;}
     public T Value {get;}
}

a następnie użyj go jako

NullableClass<Customer> customer = dbRepository.GetCustomer('Mr. Smith');
if(customer.HasValue){
  // one logic with customer.Value
}else{
  // another logic
} 

lub użyj dwóch opcji stylu z metodami rozszerzenia C #:

customer.Do(
      // code with normal behaviour
      ,
      // what to do in case of null
) 

Różnica jest znacząca. Jako klient metody wiesz, czego się spodziewać. Zespół może mieć regułę:

Jeśli klasa nie jest typu NullableClass, wówczas jej instancja nie może mieć wartości NULL .

Zespół może wzmocnić ten pomysł, stosując Design by Contract i sprawdzanie statyczne w czasie kompilacji, np. Z warunkiem:

function SaveCustomer([NotNullAttribute]Customer customer){
     // there is no need to check whether customer is null 
     // it is a client problem, not this supplier
}

lub na ciąg

function GetCustomer([NotNullAndNotEmptyAttribute]String customerName){
     // there is no need to check whether customerName is null or empty 
     // it is a client problem, not this supplier
}

Takie podejście może drastycznie zwiększyć niezawodność aplikacji i jakość oprogramowania. Design by Contract to przypadek logiki Hoare'a , którą zasadził Bertrand Meyer w swojej słynnej książce Object-Oriented Software Construction i języku Eiffel w 1988 roku, ale nie jest ona wykorzystywana nieprawidłowo we współczesnym tworzeniu oprogramowania.

Artru
źródło
0

Nie.

Nieuwzględnienie przez język specjalnej wartości, takiej jak nullproblem języka. Jeśli spojrzysz na języki takie jak Kotlin lub Swift , które zmuszają pisarza do jawnego radzenia sobie z możliwością zerowej wartości przy każdym spotkaniu, nie ma niebezpieczeństwa.

W takich językach, jak ten, język ten może nullbyć wartością dowolnego typu -ish , ale nie zapewnia żadnych konstrukcji pozwalających na potwierdzenie tego później, co prowadzi do nullnieoczekiwanych zdarzeń (szczególnie gdy są przekazywane tobie skądinąd), i w ten sposób dochodzi do awarii. To jest zło: pozwalanie ci korzystać nullbez zmuszania cię do radzenia sobie z tym.

Ben Leggiero
źródło
-1

Zasadniczo główna idea polega na tym, że problemy z odwołaniem do wskaźnika zerowego powinny zostać złapane w czasie kompilacji zamiast w czasie wykonywania. Więc jeśli piszesz funkcję, która pobiera jakiś obiekt i nie chcesz, aby ktokolwiek wywoływał ją odwołaniami zerowymi, system typów powinien pozwolić ci określić to wymaganie, aby kompilator mógł go użyć, aby zapewnić gwarancję, że wywołanie funkcji nigdy skompiluj, jeśli dzwoniący nie spełnia Twoich wymagań. Można to zrobić na wiele sposobów, na przykład dekorując typy lub używając typów opcji (w niektórych językach nazywanych także typami zerowalnymi), ale oczywiście jest to o wiele więcej pracy dla projektantów kompilatorów.

Wydaje mi się, że jest zrozumiałe, dlaczego w latach 60. i 70. projektant kompilatora nie wdał się w całości, aby wdrożyć ten pomysł, ponieważ zasoby maszyn były ograniczone, kompilatory musiały być stosunkowo proste i nikt tak naprawdę nie wiedział, jak źle to się skończy 40 lata później.

Shital Shah
źródło
-1

Mam jeden prosty sprzeciw wobec null:

Całkowicie przełamuje semantykę kodu, wprowadzając niejednoznaczność .

Często spodziewasz się, że wynik będzie określonego rodzaju. To jest semantycznie rozsądne. Powiedzmy, że poprosiłeś bazę danych o użytkownika z pewnym id, że spodziewasz się, że resut będzie określonego typu (= user). Ale co, jeśli nie ma użytkownika tego identyfikatora? Jednym (złym) rozwiązaniem jest: dodanie nullwartości. Wynik jest zatem niejednoznaczny : albo jest, useralbo jest null. Ale nullto nie jest oczekiwany typ. I tu zaczyna się zapach kodu .

Unikając null, uczynisz swój kod semantycznie czystym.

Nie zawsze są sposoby wokół null: refactoring do kolekcji Null-objects, optionalsetc.

Thomas Junk
źródło
-2

Jeśli semantycznie możliwe będzie uzyskanie dostępu do miejsca przechowywania typu odniesienia lub wskaźnika przed uruchomieniem kodu w celu obliczenia jego wartości, nie ma idealnego wyboru, co powinno się stać. Posiadanie takich lokalizacji domyślnie zerowych referencji wskaźnika, które można swobodnie czytać i kopiować, ale które zepsują się, jeśli zostaną usunięte lub zindeksowane, jest jednym z możliwych rozwiązań. Inne opcje obejmują:

  1. Domyślnie lokalizacje mają wskaźnik zerowy, ale nie pozwalają na to, aby kod je przeglądał (a nawet ustalał, że są puste) bez awarii. Możliwe, że zapewnią wyraźny sposób ustawienia wskaźnika na wartość NULL.
  2. Ustaw lokalizację domyślnie na zerowy wskaźnik i zawieszaj się, jeśli spróbujesz ją odczytać, ale zapewnij niezawodny sposób na sprawdzenie, czy wskaźnik ma zerowy wskaźnik. Zapewnij także wyraźne sposoby ustawiania wskaźnika na wartość NULL.
  3. Ustaw lokalizację domyślnie na wskaźnik do określonej instancji domyślnej wskazanego typu dostarczonej przez kompilator.
  4. Ustaw lokalizację domyślnie na wskaźnik do określonej instancji domyślnej wskazanego typu dostarczonej przez program.
  5. Uzyskaj dostęp do wskaźnika zerowego (nie tylko operacje dereferencyjne) wywołuj procedurę dostarczoną przez program, aby dostarczyć instancję.
  6. Zaprojektuj semantykę języka tak, aby nie istniały zbiory lokalizacji pamięci, z wyjątkiem tymczasowego niedostępnego kompilatora, dopóki procedury inicjalizacji nie zostaną uruchomione na wszystkich elementach (coś takiego jak konstruktor tablicy musiałby zostać dostarczony z instancją domyślną, funkcja, która zwróci instancję lub ewentualnie parę funkcji - jedną, która zwróci instancję, a druga, która byłaby wywoływana w przypadku wcześniej zbudowanych instancji, jeśli wystąpi wyjątek podczas budowy tablicy).

Ostatni wybór może mieć jakiś apel, szczególnie jeśli język zawiera zarówno typy zerowalne, jak i nie dopuszczające wartości zerowych (można wywoływać specjalne konstruktory tablicowe dla dowolnych typów, ale trzeba je wywoływać tylko podczas tworzenia tablic typów niedopuszczalnych), ale prawdopodobnie nie byłoby to wykonalne w czasie, gdy wynaleziono zerowe wskaźniki. Spośród innych wyborów żadna nie wydaje się bardziej atrakcyjna niż zezwolenie na kopiowanie zerowych wskaźników, ale bez zaniedbywania lub indeksowania. Podejście nr 4 może być wygodne jako opcja i powinno być dość tanie do wdrożenia, ale z pewnością nie powinno być jedyną opcją. Wymaganie, aby wskaźniki musiały domyślnie wskazywać jakiś konkretny prawidłowy obiekt, jest o wiele gorsze niż posiadanie wskaźników domyślnych na wartość zerową, którą można odczytać lub skopiować, ale nie można wyrejestrować ani zindeksować.

supercat
źródło
-2

Tak, NULL to okropny projekt w świecie zorientowanym obiektowo. Krótko mówiąc, użycie NULL prowadzi do:

  • obsługa błędów ad-hoc (zamiast wyjątków)
  • niejednoznaczny semantyczny
  • powoli zamiast szybko zawodzić
  • myślenie komputerowe a myślenie obiektowe
  • zmienne i niekompletne obiekty

Sprawdź ten post na blogu, aby uzyskać szczegółowe wyjaśnienie: http://www.yegor256.com/2014/05/13/why-null-is-bad.html

yegor256
źródło