Czytałem o (nie) wygodzie posiadania null
zamiast (na przykład) Maybe
. Po przeczytaniu tego artykułu , jestem przekonany, że byłoby znacznie lepiej do użytkuMaybe
(lub coś podobnego). Jestem jednak zaskoczony, widząc, że wszystkie „dobrze znane” imperatywne lub obiektowe języki programowania nadal używają null
(co pozwala na niekontrolowany dostęp do typów, które mogą reprezentować wartość „nic”), i które Maybe
są najczęściej używane w funkcjonalnych językach programowania.
Jako przykład spójrz na następujący kod C #:
void doSomething(string username)
{
// Check that username is not null
// Do something
}
Coś tu pachnie źle ... Dlaczego powinniśmy sprawdzać, czy argument jest zerowy? Czy nie powinniśmy zakładać, że każda zmienna zawiera odwołanie do obiektu? Jak widać, problem polega na tym, że z definicji prawie wszystkie zmienne mogą zawierać odwołanie zerowe. Co jeśli moglibyśmy zdecydować, które zmienne są „zerowalne”, a które nie? To zaoszczędziłoby nam dużo wysiłku podczas debugowania i szukania „NullReferenceException”. Wyobraź sobie, że domyślnie żaden typ nie może zawierać pustego odwołania . Zamiast tego powiedziałbyś wyraźnie, że zmienna może zawierać odwołanie zerowe , tylko jeśli naprawdę go potrzebujesz. Taki jest właśnie pomysł. Jeśli masz funkcję, która w niektórych przypadkach zawodzi (na przykład dzielenie przez zero), możesz zwrócić aMaybe<int>
, stwierdzając wprost, że wynikiem może być int, ale także nic! To jeden z powodów, dla których wolę Być może zamiast wartości zerowej. Jeśli interesuje Cię więcej przykładów, sugeruję przeczytać ten artykuł .
Fakty są takie, że pomimo wad polegających na domyślnym ustawianiu większości typów na null, większość języków programowania OO faktycznie to robi. Dlatego zastanawiam się nad:
- Jakie argumenty musiałbyś wdrożyć
null
w swoim języku programowania zamiastMaybe
? Czy są w ogóle jakieś powody, czy to tylko „bagaż historyczny”?
Przed udzieleniem odpowiedzi na to pytanie upewnij się, że rozumiesz różnicę między wartością null a Może.
null
lub jego koncepcja nie istnieje (IIRC Haskell jest jednym z takich przykładów).null
od dłuższego czasu. Nie jest łatwo po prostu to porzucić.Odpowiedzi:
Uważam, że jest to przede wszystkim bagaż historyczny.
Najbardziej znanym i najstarszym językiem
null
jest C i C ++. Ale tutajnull
ma to sens. Wskaźniki są nadal dość liczbowe i niskopoziomowe. I jak ktoś inny powiedział, w myśleniu programistów C i C ++, konieczność jawnego mówienia, że wskaźnik może byćnull
, nie ma sensu.Na drugim miejscu jest Java. Biorąc pod uwagę, że programiści Java starali się zbliżyć do C ++, aby ułatwić sobie przejście z C ++ na Javę, prawdopodobnie nie chcieli zadzierać z taką podstawową koncepcją języka. Ponadto wdrożenie jawne
null
wymagałoby znacznie więcej wysiłku, ponieważ po inicjalizacji należy sprawdzić, czy odwołanie inne niż null jest ustawione poprawnie.Wszystkie pozostałe języki są takie same jak Java. Zwykle kopiują tak, jak robi to C ++ lub Java, a biorąc pod uwagę, jak podstawowe jest pojęcie niejawnych
null
typów referencji, naprawdę ciężko jest zaprojektować język, który używa jawnegonull
.źródło
null
a myślę, że usunięcie nie byłoby tak dużą zmianą.std::string
nie może byćnull
.int&
nie może byćnull
. Puszkaint*
i C ++ pozwala na niekontrolowany dostęp do niego z dwóch powodów: 1. ponieważ C zrobił to i 2. ponieważ powinieneś zrozumieć, co robisz, używając surowych wskaźników w C ++.Właściwie
null
to świetny pomysł. Biorąc pod uwagę wskaźnik, chcemy zaznaczyć, że ten wskaźnik nie odwołuje się do prawidłowej wartości. Więc bierzemy jedną lokalizację pamięci, stwierdzamy, że jest niepoprawna i trzymamy się tej konwencji (konwencja czasami egzekwowana za pomocą segfault). Teraz, gdy mam wskaźnik, mogę sprawdzić, czy zawieraNothing
(ptr == null
) lubSome(value)
(ptr != null
,value = *ptr
). Chcę, żebyś zrozumiał, że jest to równoważne zMaybe
typem.Problemy z tym są następujące:
W wielu językach system typów nie pomaga tutaj zagwarantować odwołania o wartości innej niż null.
Jest to bagaż historyczny, ponieważ wiele języków głównego nurtu lub języków OOP miało tylko postępowe postępy w swoich systemach typów w porównaniu z poprzednikami. Małe zmiany mają tę zaletę, że łatwiej nauczyć się nowych języków. C # to główny język, który wprowadził narzędzia na poziomie językowym, aby lepiej obsługiwać wartości zerowe.
Projektanci API mogą powrócić
null
po awarii, ale nie odnoszą się do samej rzeczy po sukcesie. Często rzecz (bez odniesienia) jest zwracana bezpośrednio. To spłaszczenie o jeden poziom wskaźnika uniemożliwia użycienull
wartości.Jest to po prostu lenistwo po stronie projektanta i nie można na to poradzić bez narzucenia odpowiedniego zagnieżdżenia za pomocą odpowiedniego systemu typów. Niektóre osoby mogą również próbować uzasadnić to względami wydajności lub istnieniem opcjonalnych kontroli (kolekcja może zwrócić
null
lub sam element, ale także podaćcontains
metodę).W Haskell jest ładny widok na
Maybe
typ jako monadę. Ułatwia to tworzenie transformacji na podstawie zawartej wartości.Z drugiej strony języki niskiego poziomu, takie jak C, ledwo traktują tablice jako osobny typ, więc nie jestem pewien, czego się spodziewamy. W językach OOP ze sparametryzowanym polimorfizmem
Maybe
typ sprawdzany w środowisku wykonawczym jest raczej trywialny do wdrożenia.źródło
null
mniej niż kiedyś?null
s ” - mówię oNullable
typach i??
domyślnym operatorze. Nie rozwiązuje to teraz problemu w obecności starszego kodu, ale jest krokiem w kierunku lepszej przyszłości.Nullable
.Rozumiem, że
null
była to konstrukcja niezbędna do wyodrębnienia języków programowania z asemblera. 1 Programiści potrzebowali umiejętności wskazywania, że wartość wskaźnika lub rejestru byłanot a valid value
inull
stała się powszechnym terminem dla tego znaczenia.Wzmacniając punkt, który
null
jest tylko konwencją reprezentującą koncepcję, rzeczywista wartość,null
którą można było / może zmieniać się w zależności od języka programowania i platformy.Jeśli projektowałeś nowy język i chciałeś go unikać,
null
alemaybe
zamiast tego używaj , zachęciłbym bardziej opisowy termin, taki jaknot a valid value
lubnavv
wskazujący zamiar. Ale nazwa tej nie-wartości jest odrębnym pojęciem od tego, czy powinieneś pozwolić, aby nie-wartości istniały nawet w twoim języku.Zanim zdecydujesz się na jeden z tych dwóch punktów, musisz zdefiniować, co
maybe
to znaczy dla twojego systemu. Może się okazać, że jest to tylko zmiananull
znaczenianot a valid value
lub może mieć inną semantyczną dla twojego języka.Podobnie decyzja, czy sprawdzić dostęp pod kątem
null
dostępu czy referencji, jest kolejną decyzją dotyczącą projektu w Twoim języku.Aby dostarczyć trochę historii,
C
zakładałem, że programiści rozumieli, co próbowali zrobić, manipulując pamięcią. Ponieważ była to abstrakcja nadrzędna w stosunku do asemblera i poprzedzających go języków imperatywnych, zaryzykowałbym, że nie przyszła im do głowy myśl o bezpiecznej ochronie programisty przed niewłaściwym odniesieniem.Uważam, że niektóre kompilatory lub ich dodatkowe narzędzia mogą zapewnić kontrolę przed nieprawidłowym dostępem do wskaźnika. Dlatego inni zauważyli ten potencjalny problem i podjęli środki w celu jego ochrony.
To, czy powinieneś na to pozwolić, zależy od tego, co chcesz osiągnąć w swoim języku i jaki poziom odpowiedzialności chcesz nałożyć na użytkowników Twojego języka. Zależy to również od umiejętności stworzenia kompilatora w celu ograniczenia tego rodzaju zachowań.
Aby odpowiedzieć na twoje pytania:
„Jakie argumenty…” - Cóż, zależy to od tego, co chcesz zrobić w języku. Jeśli chcesz zasymulować dostęp bez dostępu metalu, możesz zezwolić na to.
„czy to tylko bagaż historyczny?” Być może nie.
null
z pewnością miał / ma znaczenie dla wielu języków i pomaga kierować wyrażaniem tych języków. Historyczny precedens mógł wpłynąć na nowsze języki i ich dopuszczenie,null
ale trochę więcej jest machać rękami i zadeklarować koncepcję bezużytecznego bagażu historycznego.1 Zobacz ten artykuł w Wikipedii, chociaż Hoare jest uznawany za wartości zerowe i języki obiektowe. Wierzę, że imperatywne języki rozwijały się wzdłuż innego drzewa genealogicznego niż Algol.
źródło
null
).object o = null; o.ToString();
kompiluje się dla mnie dobrze, bez błędów i ostrzeżeń w VS2012. ReSharper narzeka na to, ale to nie jest kompilator.Jeśli spojrzysz na przykłady w cytowanym artykule, większość czasu używania
Maybe
go nie skraca kodu. Nie eliminuje to konieczności sprawdzaniaNothing
. Jedyną różnicą jest to, że przypomina ci to za pomocą systemu typów.Uwaga: mówię „przypominaj”, a nie wymuszaj. Programiści są leniwi. Jeśli programista jest przekonany, że wartość nie może być
Nothing
, odłóż dereferencjęMaybe
bez jej sprawdzania, tak jak teraz wyłuskuje wskaźnik zerowy. Rezultatem końcowym jest przekonwertowanie wyjątku wskaźnika zerowego na wyjątek „może być dereferencjonowany pusty”.Ta sama zasada ludzkiej natury ma zastosowanie w innych obszarach, w których języki programowania próbują zmusić programistów do zrobienia czegoś. Na przykład projektanci Javy próbowali zmusić ludzi do obsługi większości wyjątków, co spowodowało powstanie wielu podejrzeń, które po cichu ignorują lub ślepo propagują wyjątki.
To, co sprawia, że
Maybe
jest miłe, to kiedy wiele decyzji jest podejmowanych poprzez dopasowanie wzorca i polimorfizm zamiast jawnych kontroli. Na przykład możesz utworzyć osobne funkcjeprocessData(Some<T>)
i zprocessData(Nothing<T>)
którymi nie możesz sobie poradzićnull
. Automatycznie przenosisz obsługę błędów do osobnej funkcji, co jest bardzo pożądane w programowaniu funkcjonalnym, w którym funkcje są przekazywane i oceniane leniwie, a nie zawsze wywoływane odgórnie. W OOP preferowanym sposobem oddzielenia kodu obsługi błędów są wyjątki.źródło
const
, ale uczynić to opcjonalnym. Niektóre rodzaje kodu niższego poziomu, takie jak na przykład połączone listy, byłyby bardzo denerwujące w przypadku implementacji z obiektami niepozwalającymi na zerowanie.map :: Maybe a -> (a -> b)
i funkcjebind :: Maybe a -> (a -> Maybe b)
, dzięki czemu można kontynuować i wykonywać wątki dalszych obliczeń w przeważającej części przy ponownym przetwarzaniu za pomocą instrukcji if else. IgetValueOrDefault :: Maybe a -> (() -> a) -> a
pozwala obsłużyć zerowy przypadek. Jest znacznie bardziej elegancki niżMaybe a
jednoznaczne dopasowanie wzoru .Maybe
jest bardzo funkcjonalnym sposobem myślenia o problemie - istnieje rzecz, która może, ale nie musi, mieć określoną wartość. Jednak w sensie obiektowym zastępujemy tę koncepcję rzeczy (bez względu na to, czy ma ona wartość, czy nie) na obiekt. Oczywiście obiekt ma wartość. Jeśli nie, mówimy, że obiekt jestnull
, ale tak naprawdę rozumiemy to, że w ogóle nie ma żadnego obiektu. Odwołanie do obiektu nie wskazuje na nic. TłumaczenieMaybe
na koncepcję OO nie jest niczym nowym - w rzeczywistości tworzy po prostu znacznie bardziej zaśmiecony kod. Nadal musisz mieć zerowe odwołanie do wartościMaybe<T>
. Nadal musisz wykonać kontrole zerowe (w rzeczywistości musisz wykonać znacznie więcej kontroli zerowych, zaśmiecając kod), nawet jeśli są one teraz nazywane „być może czekami”. Oczywiście, napiszesz bardziej solidny kod, jak twierdzi autor, ale twierdzę, że tak jest tylko dlatego, że uczyniłeś język bardziej abstrakcyjnym i tępym, wymagającym poziomu pracy, który w większości przypadków jest niepotrzebny. Od czasu do czasu chętnie biorę wyjątek NullReferenceException, niż zajmuję się kodem spaghetti, który sprawdza, może za każdym razem, gdy uzyskuję dostęp do nowej zmiennej.źródło
Maybe<T>
musi zezwolićnull
jako wartość, ponieważ wartość może nie istnieć. Jeśli takMaybe<MyClass>
, i nie ma wartości, pole wartości musi zawierać odwołanie zerowe. Nie ma nic, co mogłoby być w pełni bezpieczne.Maybe
może to być klasa abstrakcyjna, z dwiema dziedziczącymi po niej klasami:Some
(która ma pole dla wartości) iNone
(która nie ma tego pola). W ten sposób wartość nigdy nie jestnull
.Pojęcie
null
można łatwo prześledzić z powrotem do C, ale nie na tym polega problem.Moim codziennym językiem jest C # i zachowałbym
null
jedną różnicę. C # ma dwa rodzaje typów, wartości i referencje . Wartości nigdy nie mogą byćnull
, ale czasami chciałbym wyrazić, że żadna wartość nie jest w porządku. Aby to zrobić, C # używaNullable
typów, więcint
byłaby to wartość iint?
wartość zerowa. Tak myślę, że typy referencyjne powinny również działać.Zobacz także: Odwołanie zerowe nie może być pomyłką :
źródło
Myślę, że dzieje się tak, ponieważ programowanie funkcjonalne jest bardzo zaniepokojone typami , szczególnie tymi, które łączą inne typy (krotki, funkcje jako typy pierwszej klasy, monady itp.) Niż programowanie obiektowe (lub przynajmniej początkowo tak zrobiło).
Nowoczesne wersje języków programowania, o których myślę, że mówisz (C ++, C #, Java), oparte są na językach, które nie miały żadnej formy programowania ogólnego (C, C # 1.0, Java 1). Bez tego nadal można upiec w języku pewną różnicę między obiektami zerowalnymi i nierozpoznawalnymi (takimi jak odwołania do C ++, które nie mogą być
null
, ale są również ograniczone), ale jest to o wiele mniej naturalne.źródło
Myślę, że podstawowym powodem jest to, że stosunkowo niewiele kontroli zerowych jest wymaganych, aby program był „bezpieczny” przed uszkodzeniem danych. Jeśli program próbuje użyć zawartości elementu tablicy lub innej lokalizacji pamięci, która powinna zostać zapisana z prawidłowym odwołaniem, ale nie została wykonana, najlepszym rozwiązaniem jest zgłoszenie wyjątku. Idealnie, wyjątek wskaże dokładnie, gdzie wystąpił problem, ale ważne jest, że jakiś wyjątek zostanie zgłoszony, zanim odwołanie zerowe zostanie zapisane gdzieś, co może spowodować uszkodzenie danych. O ile metoda nie przechowuje obiektu bez wcześniejszego użycia go w jakiś sposób, próba użycia obiektu będzie sama w sobie stanowić swego rodzaju „kontrolę zerową”.
Jeśli ktoś chce się upewnić, że odwołanie zerowe, które pojawia się tam, gdzie nie powinno, spowoduje określony wyjątek inny niż
NullReferenceException
, często konieczne będzie włączenie kontroli zerowej w całym miejscu. Z drugiej strony samo upewnienie się, że wystąpi jakiś wyjątek, zanim odwołanie zerowe może spowodować „uszkodzenie” wykraczające poza to, co już zostało zrobione, często wymaga stosunkowo niewielu testów - testowanie byłoby zwykle wymagane tylko w przypadkach, w których obiekt przechowywałby odniesienia, nie próbując go używać, i albo odniesienie zerowy byłoby zastąpić ważny jeden, lub spowodowałoby to inny kod błędnie interpretować inne aspekty stanu programu. Takie sytuacje istnieją, ale nie są tak powszechne; większość przypadkowych zerowych odniesień zostanie bardzo szybko złapanaczy ktoś je sprawdza, czy nie .źródło
„
Maybe
,” jak napisano, jest konstrukcją wyższego poziomu niż zero. Używając więcej słów, aby to zdefiniować, być może „wskaźnik do rzeczy lub wskaźnik do niczego, ale kompilator nie otrzymał jeszcze wystarczających informacji, aby ustalić, który”. To zmusza cię do ciągłego sprawdzania każdej wartości, chyba że zbudujesz specyfikację kompilatora, która jest wystarczająco inteligentna, aby nadążyć za pisanym kodem.Możesz wykonać implementację Może w języku, który ma wartości zerowe. C ++ ma jeden w postaci
boost::optional<T>
. Wyrównanie wartości null z Może jest bardzo trudne. W szczególności, jeśli mamMaybe<Just<T>>
, nie mogę przypisać go do wartości null (ponieważ taka koncepcja nie istnieje), podczas gdyT**
w języku o wartości null bardzo łatwo jest przypisać wartość null. To zmusza do użyciaMaybe<Maybe<T>>
, co jest całkowicie poprawne, ale zmusi cię do wykonania o wiele więcej kontroli, aby użyć tego obiektu.Niektóre języki funkcjonalne używają Może dlatego, że wartość null wymaga niezdefiniowanego zachowania lub obsługi wyjątków, z których żaden nie jest łatwym pojęciem do mapowania na składnie języka funkcjonalnego. Być może rolę tę pełni lepiej w takich sytuacjach funkcjonalnych, ale w językach proceduralnych null jest królem. Nie jest to kwestia dobra i zła, tylko kwestia tego, co ułatwia powiedzenie komputerowi, aby zrobił to, co chcesz.
źródło