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?
language-design
exceptions
pointers
null
Tim Goodman
źródło
źródło
Odpowiedzi:
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 value
lubNONE
.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.
źródło
String
Typ nie jest tak naprawdę ciągiem. ToString or Null
typ. 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 wymagaString
(lub cokolwiek innego), musi on mieć wartość.Option
Typ 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.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
if
instrukcjami 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.
źródło
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.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.
źródło
[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.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:
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.źródło
GetByLastName
zwraca null, jeśli nie zostanie znaleziony (w przeciwieństwie do zgłaszania wyjątku) - po prostu widzę, czy zwracaCustomer?
lubCustomer
.Maybe
- AMaybe Customer
to alboJust c
(gdziec
jest aCustomer
) alboNothing
. W innych językach nazywa się toOption
- zobacz niektóre inne odpowiedzi na to pytanie.Customer.FirstName
jest typuString
(w przeciwieństwie doString?
). To jest prawdziwa korzyść - tylko zmienne / właściwości, które mają znaczącą wartość nic, powinny być zadeklarowane jako zerowalne.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:I możesz sprawdzić takie zmienne:
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, powiedzmynull
. Ale dlaczego miałbyś to zrobić? Zmienna typuFoo
powinna zawierać tylkoFoo
s - to jest cały punkt systemu typów!null
nie jestFoo
niczym więcej niż"Hello, world!"
jest. Co gorsza,null
nie 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 jakoFoo
s. Zauważ, że sprawdzanie ciągu w poprzednim fragmencie nie propaguje faktu, że foo1 jest naprawdęFoo
-bar
prawdopodobnie również będzie miał własne sprawdzanie, dla bezpieczeństwa.Porównaj to z użyciem typu
Maybe
/Option
z dopasowaniem wzorca:Wewnątrz
Just foo
klauzuli zarówno Ty, jak i program na pewno wiesz, że naszaMaybe Foo
zmienna naprawdę zawieraFoo
wartość - ta informacja jest propagowana w dół łańcucha wywołań ibar
nie musi wykonywać żadnych kontroli. PonieważMaybe Foo
jest to odmienny typ odFoo
, jesteś zmuszony poradzić sobie z możliwością, że może on zawieraćNothing
, więc nigdy nie możesz być zaskoczony przezNullPointerException
. Możesz znacznie łatwiej zrozumieć swój program, a kompilator może pominąć sprawdzanie wartości NULL, wiedząc, że wszystkie zmienne typuFoo
naprawdę zawierająFoo
s. Wszyscy wygrywają.źródło
my Int $variable = Int
, gdzieInt
jest pusta wartość typuInt
?this type only contains integers
w przeciwieństwie do niegothis type contains integers and this other value
, będziesz mieć problem za każdym razem, gdy chcesz tylko liczb całkowitych.??
,?.
itd.) Dla rzeczy, które języki takie jak haskell implementują jako funkcje biblioteczne. Myślę, że jest o wiele ładniejszy.null
/Nothing
propagation nie jest w żaden sposób „specjalny”, więc dlaczego powinniśmy nauczyć się więcej składni, aby go mieć?(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óż, UNLESSt
jest zerowy w czasie wykonywania. Teraz wszystkie zakłady są wyłączone. Kompilator powiedział, że jest OK, ale dowiaduję się później, żet
tak naprawdę NIEdoSomething()
. 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 / 0
w 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:
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”.
źródło
*int
identyfikuje alboint
, alboNULL
. Podobnie w C ++, zmienna typu*Widget
albo identyfikujeWidget
rzecz, której typ pochodziWidget
lubNULL
. Problem z Java i pochodne jak C # jest to, że używają oni miejsce przechowywaniaWidget
znaczyć*Widget
. Rozwiązaniem nie jest udawanie, że*Widget
nie powinien być w stanie pomieścićNULL
, ale raczej odpowiedniWidget
typ lokalizacji przechowywania.null
Wskaźnik jest narzędziemnie 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”.
null
Wskaźnik nie jest tak daleko od tego.EDYCJA: Chociaż
NullObject
wzorzec wydaje się lepszym rozwiązaniem niż odwołania, które mogą być zerowe, sam wprowadza problemy:Odwołanie przechowujące
NullObject
powinno (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 przypadkuNullObject
zachowania, 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 przezout
. Powiedzmy, że zwracana jest wartośćint
i 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ńcuNullObject
nic nie powinno robić, ale kiedy zostanie poproszony o wartość, musi jakoś zareagować. Niepowodzenie nie jest opcją: używamy,NullObject
aby 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
NullObject
na 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.źródło
null
powinien 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”.Maybe T
która może zawierać albo wartośćNothing
, alboT
wartość. Kluczową kwestią jest to, że musisz sprawdzić, czyMaybe
naprawdę zawiera jakiś element,T
zanim 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 jestMaybe
.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ściemt
na tę metodę? Dzięki typowi opcji kompilator wie, czy sprawdziłeś już, czy nie, patrząc na typt
. Jeśli jest to opcja, jej nie zaznaczyłeś, a jeśli jest to wartość nieopakowana, to masz.Odwołania zerowe są błędem, ponieważ pozwalają na kod nie sensowny:
Istnieją alternatywy, jeśli wykorzystasz system typów:
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 ...źródło
Maybe
nie jest wbudowana w języku rdzenia, jego definicja właśnie dzieje się w standardowym preludium:data Maybe t = Nothing | Just t
.T
dzielenia wartości typu przez inne wartości typuT
, ograniczyłbyś operację do wartości typuT
podzielonych przez wartości typuNonZero<T>
.Opcjonalne typy i dopasowanie wzorca. Ponieważ nie znam C #, oto fragment kodu w fikcyjnym języku o nazwie Scala # :-)
źródło
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ć
Rzeczywiście, wartości zerowe są kosztowne.
źródło
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.
źródło
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ć
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
źródło
None
każdego kroku.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.
źródło
null
jest to jedyny (a nawet preferowany) sposób modelowania braku wartości.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.
źródło
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.
źródło
None
coś, co nie jest Opcją, do błędu w czasie kompilacji, a także użycieOption
tak, 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.s: String = None
, musisz powiedziećs: Option[String] = None
. A jeślis
jest 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?") }
s: String = null
, ale taka jest cena interoperacyjności z Javą. Zasadniczo zabrania się tego rodzaju rzeczy w naszym kodzie Scala.Nulls
są 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
null
jest 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ą
Options
moż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: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
Option
jestNone
. Mało tego, gdybyśmy o tym zapomnieli, nie sprawdziłby typu. Nawet wtedy odbywa się to w bardzo naturalny sposób, bez wyraźnegoif
oś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.źródło
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ł:
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ć
Generics
do zminimalizowania wpływuNull
odwołania, tworząc tylko do odczytuNullableClass<T>
(dwie opcje):a następnie użyj go jako
lub użyj dwóch opcji stylu z metodami rozszerzenia C #:
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:
lub na ciąg
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.
źródło
Nie.
Nieuwzględnienie przez język specjalnej wartości, takiej jak
null
problem 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
null
być wartością dowolnego typu -ish , ale nie zapewnia żadnych konstrukcji pozwalających na potwierdzenie tego później, co prowadzi donull
nieoczekiwanych 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ćnull
bez zmuszania cię do radzenia sobie z tym.źródło
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.
źródło
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: dodanienull
wartości. Wynik jest zatem niejednoznaczny : albo jest,user
albo jestnull
. Alenull
to 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 kolekcjiNull-objects
,optionals
etc.źródło
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ą:
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ć.
źródło
Tak, NULL to okropny projekt w świecie zorientowanym obiektowo. Krótko mówiąc, użycie NULL prowadzi do:
Sprawdź ten post na blogu, aby uzyskać szczegółowe wyjaśnienie: http://www.yegor256.com/2014/05/13/why-null-is-bad.html
źródło