Jeśli wartości zerowe są złe, co należy zastosować, gdy wartość może być znacząco nieobecna?


26

Jest to jedna z zasad, które powtarzają się w kółko i to mnie denerwuje.

Wartości zerowe są złe i należy ich unikać, gdy tylko jest to możliwe .

Ale, ale - z mojej naiwności pozwól mi krzyczeć - czasami wartość MOGĄ być wyraźnie nieobecna!

Pozwól, że zapytam o przykład, który pochodzi z tego okropnego kodu opartego na anty-wzorcach, nad którym obecnie pracuję . Jest to, w gruncie rzeczy, gra sieciowa dla wielu graczy, w której tury obu graczy są uruchamiane jednocześnie (jak w Pokemon i przeciwnie do szachów).

Po każdej kolejce serwer rozgłasza listę aktualizacji kodu JS po stronie klienta. Klasa aktualizacji:

public class GameUpdate
{
    public Player player;
    // other stuff
}

Ta klasa jest serializowana do JSON i wysyłana do podłączonych graczy.

Większość aktualizacji wiąże się oczywiście z graczem - w końcu trzeba wiedzieć, który gracz wykonał ruch w tej turze. Jednak niektóre aktualizacje nie mogą mieć z nimi istotnego powiązania. Przykład: Gra została remisowana siłą, ponieważ limit obrotów bez akcji został przekroczony. Wydaje mi się, że w przypadku takich aktualizacji zerowanie odtwarzacza ma sens.

Oczywiście mogę „naprawić” ten kod, wykorzystując dziedziczenie:

public class GameUpdate
{
    // stuff
}

public class GamePlayerUpdate : GameUpdate
{
    public Player player;
    // other stuff
}

Jednak nie widzę, jak to się poprawia, z dwóch powodów:

  • Kod JS będzie teraz po prostu odbierał obiekt bez Player jako zdefiniowaną właściwość, która jest taka sama, jakby była pusta, ponieważ oba przypadki wymagają sprawdzenia, czy wartość jest obecna, czy nie;
  • Gdyby do klasy GameUpdate dodano kolejne pole zerowalne, musiałbym mieć możliwość wielokrotnego dziedziczenia, aby kontynuować to projektowanie - ale MI jest samo w sobie złe (według bardziej doświadczonych programistów) i, co ważniejsze, C # nie mam, więc nie mogę go użyć.

Mam wrażenie, że ten fragment tego kodu jest jednym z wielu, w których doświadczony, dobry programista krzyczałby z przerażeniem. Jednocześnie nie widzę, jak w tym miejscu nic nie rani niczego i co zamiast tego należy zrobić.

Czy możesz mi wyjaśnić ten problem?


2
Czy to pytanie jest specyficzne dla JavaScript? Wiele języków ma w tym celu typ opcjonalny, ale nie wiem, czy js ma taki (chociaż można go stworzyć), czy też będzie działał z jsonem (chociaż jest to jedna z części nieprzyjemnych kontroli zerowych raczej w całym kodzie
Richard Tingle

9
Ta zasada jest po prostu fałszywa. Jestem trochę zdziwiony, że ktoś poparłby to pojęcie. W niektórych przypadkach istnieje kilka dobrych alternatyw dla wartości zerowej. null jest naprawdę dobry do reprezentowania brakującej wartości. Nie pozwól, aby takie losowe reguły internetowe uniemożliwiały ci zaufanie do własnego uzasadnionego osądu.
usr

1
@usr - całkowicie się zgadzam. Null jest twoim przyjacielem. IMHO, nie ma wyraźniejszego sposobu na przedstawienie brakującej wartości. Pomoże Ci znaleźć błędy, pomoże ci radzić sobie z błędami, może pomóc ci błagać o wybaczenie (spróbuj / złap). Czego nie lubić ?!
Scuba Steve

4
jedną z pierwszych zasad projektowania oprogramowania jest „Zawsze sprawdzaj, czy nie ma nic”. Dlaczego jest to nawet problem, zadziwiają mnie.
MattE

1
@MattE - Oczywiście. Dlaczego miałbym opracowywać wzór projektowy, skoro tak naprawdę Null to dobra rzecz w podstawowym zestawie narzędzi. To, co sugerował Graham, IMHO, wydaje mi się nadmierną inżynierią.
Scuba Steve

Odpowiedzi:


20

Dużo rzeczy lepiej jest zwrócić niż zero.

  • Pusty ciąg („”)
  • Pusta kolekcja
  • Monada „opcjonalna” lub „może”
  • Funkcja, która cicho nic nie robi
  • Obiekt pełen metod, które cicho nic nie robią
  • Znacząca wartość, która odrzuca niepoprawną przesłankę pytania
    (co sprawiło, że uznałeś, że jest ona pusta)

Sztuką jest uświadomienie sobie, kiedy to zrobić. Null jest naprawdę dobry w wysadzaniu w twarz, ale tylko wtedy, gdy go dotrzesz, nie sprawdzając go. Nie jest dobry w wyjaśnianiu, dlaczego.

Jeśli nie musisz wysadzać w powietrze i nie chcesz sprawdzać wartości zerowej, skorzystaj z jednej z tych alternatyw. Zwracanie kolekcji może wydawać się dziwne, jeśli zawiera ona zawsze 0 lub 1 elementów, ale jest naprawdę dobra w cichym radzeniu sobie z brakującymi wartościami.

Monady brzmią fantazyjnie, ale tutaj wszystko, co robią, pozwala ci dać do zrozumienia, że ​​prawidłowe rozmiary kolekcji to 0 i 1. Jeśli masz, rozważ je.

Każdy z nich lepiej radzi sobie z wyjaśnianiem intencji niż zero. To najważniejsza różnica.

Może pomóc zrozumieć, dlaczego w ogóle wracasz, niż po prostu zgłaszasz wyjątek. Wyjątki są zasadniczo sposobem na odrzucenie założenia (nie są jedynym sposobem). Kiedy pytasz o punkt, w którym przecinają się dwie linie, zakładasz, że przecinają się. Mogą być równoległe. Czy należy wprowadzić wyjątek dotyczący „linii równoległych”? Cóż, możesz, ale teraz musisz poradzić sobie z tym wyjątkiem gdzie indziej lub pozwolić mu zatrzymać system.

Jeśli wolisz pozostać tam, gdzie jesteś, możesz przekształcić odrzucenie tego założenia w rodzaj wartości, która może wyrażać wyniki lub odrzucenia. To nie jest takie dziwne. Cały czas robimy to w algebrze . Sztuką jest nadanie tej wartości znaczenia, abyśmy mogli zrozumieć, co się stało.

Jeśli zwrócona wartość może wyrażać wyniki i odrzucenia, należy ją wysłać do kodu obsługującego oba te elementy. Polimorfizm jest tutaj naprawdę potężny. Zamiast po prostu wymieniać czeki zerowe na isPresent()czeki, możesz napisać kod, który zachowuje się dobrze w obu przypadkach. Albo zastępując puste wartości wartościami domyślnymi, albo dyskretnie nic nie robiąc.

Problem z wartością null polega na tym, że może to oznaczać zbyt wiele rzeczy. Więc po prostu niszczy informacje.


W moim ulubionym eksperymencie o zerowej myśli proszę o wyobrażenie sobie złożonej maszyny Rube Goldbergian , która sygnalizuje zakodowane wiadomości za pomocą kolorowego światła, podnosząc kolorowe żarówki z przenośnika taśmowego, wkręcając je w gniazdo i zasilając je. Sygnał jest kontrolowany przez różne kolory żarówek umieszczonych na taśmie przenośnika.

Poproszono Cię o uczynienie tej ohydnie drogiej rzeczy zgodnej z RFC3.14159, która stwierdza, że ​​pomiędzy wiadomościami powinien znajdować się znacznik. Znacznik nie powinien być wcale światłem. Powinien trwać dokładnie jeden impuls. Tyle samo czasu, co zwykle świeci jedno kolorowe światło.

Nie możesz po prostu zostawić spacji między wiadomościami, ponieważ urządzenie uruchamia alarmy i zatrzymuje się, jeśli nie może znaleźć żarówki do włożenia do gniazda.

Wszyscy zaznajomieni z tym projektem drżą na myśl o dotknięciu maszyny lub jej kodu sterującego. Zmiana nie jest łatwa. Co robisz? Możesz zanurzyć się i zacząć niszczyć rzeczy. Może zaktualizuj te rzeczy ohydne wzornictwo. Tak, możesz to zrobić o wiele lepiej. Możesz też porozmawiać z dozorcą i zacząć zbierać spalone żarówki.

To rozwiązanie polimorficzne. System nie musi nawet wiedzieć, że coś się zmieniło.


To naprawdę miłe, jeśli możesz to zrobić. Kodując znaczące odrzucenie założenia w wartość, nie musisz zmuszać pytania do zmiany.

Ile jabłek będziesz mi winien, jeśli dam ci 1 jabłko? -1. Ponieważ byłam ci winna 2 jabłka z wczoraj. Tutaj założenie pytania „jesteś mi winien” jest odrzucone liczbą ujemną. Mimo że pytanie było błędne, w tej odpowiedzi zakodowane są znaczące informacje. Odrzucając je w znaczący sposób, pytanie nie jest zmuszane do zmiany.

Czasami jednak zmiana pytania jest właściwą drogą. Jeśli założenie może być bardziej wiarygodne, a jednocześnie przydatne, rozważ to.

Problem polega na tym, że zwykle aktualizacje pochodzą od graczy. Ale odkryłeś potrzebę aktualizacji, która nie pochodzi od odtwarzacza 1 lub odtwarzacza 2. Potrzebujesz aktualizacji z informacją, że upłynął czas. Żaden z graczy tego nie mówi, więc co powinieneś zrobić? Pozostawienie gracza zerowego wydaje się takie kuszące. Ale niszczy informacje. Próbujesz zakodować wiedzę w czarnej dziurze.

Pole gracza nie mówi, kto się właśnie przeprowadził. Myślisz, że tak, ale ten problem dowodzi, że to kłamstwo. Pole gracza informuje, skąd pochodzi aktualizacja. Aktualizacja, której upłynął czas, pochodzi z zegara gry. Moim zaleceniem jest nadanie zegarowi zasługi, na jaką zasługuje, i zmiana nazwy playerpola na coś podobnego source.

W ten sposób, jeśli serwer się wyłącza i musi zawiesić grę, może wysłać aktualizację, która informuje o tym, co się dzieje, i podaje się jako źródło aktualizacji.

Czy to oznacza, że ​​nigdy nie powinieneś po prostu coś pomijać? Nie. Ale musisz wziąć pod uwagę, jak dobrze nie można założyć, że naprawdę oznacza pojedynczą dobrze znaną dobrą wartość domyślną. Nie jest tak naprawdę zbyt łatwe w użyciu. Dlatego zachowaj ostrożność podczas korzystania z niego. Istnieje szkoła myślenia, która nie sprzyja niczym . Nazywa się to konwencją nad konfiguracją .

Sam to lubię, ale tylko wtedy, gdy konwencja jest w jakiś sposób jasno zakomunikowana. Nie powinno to być sposobem na zmylenie nowicjuszy. Ale nawet jeśli tego używasz, null wciąż nie jest najlepszym sposobem na zrobienie tego. Null jest niebezpiecznym niczym . Null udaje, że można z niego korzystać aż do momentu użycia, a następnie wysadza dziurę we wszechświecie (psuje model semantyczny). Chyba że zachowanie jest potrzebne, chyba że zrujnowanie dziury we wszechświecie jest powodem, dla którego masz z tym problem? Użyj czegoś innego.


9
„zwróć kolekcję, jeśli tylko ma 0 lub 1 elementów” - przepraszam, ale nie sądzę, że ją otrzymuję :( Z mojego POV zrobienie tego (a) dodaje niepotrzebne niepoprawne stany do klasy (b) wymaga udokumentowania, że kolekcja musi mieć co najwyżej 1 element (c) i tak nie wydaje się rozwiązać problemu, ponieważ sprawdzanie, czy kolekcja zawiera swój jedyny element przed uzyskaniem dostępu, jest konieczne, w przeciwnym razie zostanie
zgłoszony

2
@gaazkam zobaczyć? Mówiłem ci, że to wkurza ludzi. Ale to dokładnie to, co robiłeś za każdym razem, gdy używasz pustego łańcucha.
candied_orange

16
Co dzieje się, gdy istnieje poprawna wartość, która jest pustym ciągiem lub zestawem?
Blrfl,

5
W każdym razie @gaazkam, nie sprawdzasz wyraźnie, czy kolekcja zawiera elementy. Pętlowanie (lub mapowanie) na pustej liście jest bezpieczną czynnością, która nie ma żadnego efektu.
RubberDuck

3
Wiem, że odpowiadasz na pytanie, co powinien zrobić ktoś, zamiast zwracać wartość zerową, ale naprawdę nie zgadzam się z wieloma z tych punktów. Ponieważ jednak nie jesteśmy specyficzni dla danego języka i istnieje powód, aby łamać każdą zasadę, której odmawiam głosowania
Liath,

15

nulltak naprawdę nie jest tutaj problemem. Problem polega na tym, jak większość obecnych języków programowania radzi sobie z brakującymi informacjami; nie traktują tego problemu poważnie (lub przynajmniej nie zapewniają wsparcia i bezpieczeństwa, których większość ziemian potrzebuje, aby uniknąć pułapek). Prowadzi to do wszystkich problemów, których nauczyliśmy się bać (wyjątek Java NullPointerException, Segfaults ...).

Zobaczmy, jak lepiej poradzić sobie z tym problemem.

Inni sugerowali wzorzec zerowego obiektu . Chociaż wydaje mi się, że ma to czasem zastosowanie, istnieje wiele scenariuszy, w których nie ma tylko jednej wartości domyślnej dla jednego rozmiaru. W takich przypadkach konsumenci ewentualnie nieobecnych informacji muszą mieć możliwość samodzielnego podjęcia decyzji, co zrobić, jeśli brakuje tych informacji.

Istnieją języki programowania, które zapewniają bezpieczeństwo przed wskaźnikami zerowymi (tzw. Void security) w czasie kompilacji. Podam kilka współczesnych przykładów: Kotlin, Rust, Swift. W tych językach problem braku informacji został potraktowany poważnie, a użycie wartości null jest całkowicie bezbolesne - kompilator powstrzyma cię od wyłuskiwania zerowych punktów.
W Javie podjęto starania, aby ustanowić adnotacje @ Nullable / @ NotNull wraz z kompilatorem + wtyczki IDE, aby osiągnąć ten sam efekt. To nie było tak naprawdę udane, ale to nie wchodzi w zakres tej odpowiedzi.

Wyraźny, ogólny typ oznaczający możliwą nieobecność jest innym sposobem na poradzenie sobie z tym. Np. W Javie jest java.util.Optional. Może działać:
na poziomie projektu lub firmy możesz ustalić zasady / wytyczne

  • używaj tylko wysokiej jakości bibliotek stron trzecich
  • zawsze używaj java.util.Optionalzamiastnull

Twoja społeczność / ekosystem językowy wymyślił inne sposoby rozwiązania tego problemu lepiej niż kompilator / tłumacz.


Czy mógłbyś uprzejmie opracować, w jaki sposób opcje Java są lepsze od wartości zerowych? Gdy czytam dokumentację , próba uzyskania wartości z opcjonalnej powoduje wyjątek, jeśli wartość jest nieobecna; więc wydaje się, że jesteśmy w tym samym miejscu: czek jest konieczny, a jeśli go zapomnimy, to nas przerazi?
gaazkam

3
@gaazkam Masz rację, nie zapewnia bezpieczeństwa podczas kompilacji. Również sam Opcjonalny może być zerowy, co stanowi kolejny problem. Ale jest to wyraźne (w porównaniu do zmiennych, które mogą być komentarzami null / doc). Daje to tylko realną korzyść, jeśli dla całej bazy kodu jest ustawiona reguła kodowania. Nie możesz ustawić wartości na zero. Jeśli informacje mogą być nieobecne, użyj Opcjonalne. To wymusza jawność. Zwykłe kontrole zerowe stają się następnie połączeniami zOptional.isPresent
marstato,

8
@marstato zamienianie czeków zerowych na isPresent() nie jest zalecanym użyciem Opcjonalnego
candied_orange

10

Nie widzę żadnej wartości w stwierdzeniu, że wartość null jest zła. Sprawdziłem dyskusje na ten temat, a one tylko niecierpliwiły mnie i irytowały.

Null może być źle użyty, jako „magiczna wartość”, gdy w rzeczywistości coś znaczy. Ale aby się tam dostać, musiałbyś zrobić trochę pokręconego programowania.

Wartość null powinna oznaczać „nie ma”, która nie została utworzona (jeszcze) lub (już) nie ma lub nie została podana, jeśli jest argumentem. To nie jest stan, brakuje całej rzeczy. W ustawieniu OO, w którym rzeczy są tworzone i niszczone przez cały czas, jest to ważny, znaczący warunek różny od dowolnego stanu.

Z null dostajesz klapsa w czasie wykonywania, gdzie dostajesz klapsa w czasie kompilacji z jakąś bezpieczną alternatywą typu null.

Nie do końca prawda, mogę otrzymać ostrzeżenie, aby nie sprawdzać wartości null przed użyciem zmiennej. I to nie to samo. Mogę nie chcieć oszczędzać zasobów obiektu, który nie ma celu. A co ważniejsze, będę musiał sprawdzić alternatywę tak samo, aby uzyskać pożądane zachowanie. Jeśli zapomnę to zrobić, wolę uzyskać NullReferenceException w czasie wykonywania niż jakieś nieokreślone / niezamierzone zachowanie.

Jest jeden problem, o którym należy pamiętać. W kontekście wielowątkowym należałoby zabezpieczyć się przed warunkami wyścigu, przypisując zmienną lokalną przed wykonaniem sprawdzenia wartości null.

Problem z wartością zerową może oznaczać wiele rzeczy. Więc po prostu niszczy informacje.

Nie, to nic nie powinno znaczyć, więc nie ma co niszczyć. Nazwa i typ zmiennej / argumentu zawsze powie, czego brakuje, to wszystko, co trzeba wiedzieć.

Edytować:

Jestem w połowie drogi do filmu candied_orange połączonego z jedną z jego innych odpowiedzi na temat programowania funkcjonalnego i jest to bardzo interesujące. Widzę jego przydatność w niektórych scenariuszach. Podejście funkcjonalne, które do tej pory widziałem, z latającymi fragmentami kodu, zwykle powoduje, że kulę się, gdy jestem używany w ustawieniach OO. Ale chyba ma swoje miejsce. Gdzieś. I wydaje mi się, że będą języki, które wykonają dobrą robotę, ukrywając to, co uważam za mylący bałagan, ilekroć napotykam go w C #, języki, które zapewniają czysty i sprawny model.

Jeśli ten model funkcjonalny jest twoim umysłem / frameworkiem, tak naprawdę nie ma alternatywy dla popychania jakichkolwiek wpadek do przodu jako udokumentowanych opisów błędów i ostatecznie pozwól, aby osoba dzwoniąca poradziła sobie z nagromadzonymi śmieciami, które zostały przeciągnięte aż do momentu inicjacji. Najwyraźniej tak właśnie działa programowanie funkcjonalne. Nazwij to ograniczeniem, nazwij to nowym paradygmatem. Możesz uznać, że jest to hack dla funkcjonalnych uczniów, który pozwala im dalej iść na nieświętą ścieżkę, możesz być zachwycony tym nowym sposobem programowania, który uwalnia nowe ekscytujące możliwości. Cokolwiek, wydaje mi się bezcelowe wkraczanie w ten świat, wskazywanie na tradycyjny sposób robienia rzeczy przez OO (tak, możemy rzucać wyjątki, przepraszam), wybranie z tego jakiejś koncepcji

Każda koncepcja może być użyta w niewłaściwy sposób. Z mojego punktu widzenia prezentowane „rozwiązania” nie stanowią alternatywy dla właściwego użycia zer. Są to koncepcje z innego świata.


9
Null niszczy wiedzę o tym, jaki rodzaj niczego reprezentuje. Jest coś, co powinno być cicho ignorowane. Rodzaj niczego, co musi zatrzymać system, aby uniknąć nieprawidłowego stanu. Nic, co oznacza, że ​​programista spieprzył i musi naprawić kod. Nic, co oznacza, że ​​użytkownik poprosił o coś, co nie istnieje i należy grzecznie powiedzieć. Przepraszam, nie. Wszystkie zostały zakodowane jako null. Z tego powodu, jeśli zakodujesz którykolwiek z nich jako zerowy, nie powinieneś być zaskoczony, jeśli nikt nie wie, co masz na myśli. Zmuszasz nas do zgadywania z kontekstu.
candied_orange

3
@candied_orange Możesz zrobić dokładnie ten sam argument na temat dowolnego typu opcjonalnego: Czy Nonereprezentuje….
Deduplicator

3
@candied_orange Nadal nie rozumiem o co ci chodzi. Różne rodzaje niczego? Jeśli użyjesz null, kiedy powinieneś był użyć enum? Jest tylko jeden rodzaj niczego. Powód, dla którego coś jest niczym, może być różny, ale to nie zmienia nicości ani właściwej reakcji na coś, co jest niczym. Jeśli spodziewasz się wartości w sklepie, której nie ma i która jest godna uwagi, rzucasz lub logujesz w momencie zapytania i nic nie dostajesz, nie podajesz wartości null jako argumentu metody, a następnie w tej metodzie spróbuj dowiedzieć się dlaczego masz zero. Null nie byłby błędem.
Martin Maat,

2
Null jest błędem, ponieważ miałeś okazję zakodować znaczenie niczego. Twoje zrozumienie niczego nie wymaga natychmiastowej obsługi niczego. 0, null, NaN, pusty łańcuch, pusty zestaw, void, obiekt null, nie dotyczy, nie został zaimplementowany wyjątek, nic nie występuje w wielu różnych postaciach. Nie musisz zapominać o tym, co teraz wiesz, tylko dlatego, że nie chcesz zajmować się tym, co wiesz teraz. Jeśli poproszę cię o pierwiastek kwadratowy z -1, możesz albo rzucić wyjątek, aby dać mi znać, że to niemożliwe i zapomnieć o wszystkim, albo wymyślić wymyślone liczby i zachować to, co wiesz.
candied_orange

1
@MartinMaat sprawia, że ​​brzmi to tak, jakby funkcja musiała zgłaszać wyjątek za każdym razem, gdy naruszane są Twoje oczekiwania. To po prostu nieprawda. Często istnieją przypadki narożne do załatwienia. Jeśli naprawdę tego nie widzisz, przeczytaj to
candied_orange

7

Twoje pytanie.

Ale, ale - z mojej naiwności pozwól mi krzyczeć - czasami wartość MOGĄ być wyraźnie nieobecna!

W pełni tam na pokładzie. Są jednak pewne zastrzeżenia, które omówię za chwilę. Ale najpierw:

Wartości zerowe są złe i należy ich unikać, gdy tylko jest to możliwe.

Myślę, że jest to oświadczenie o dobrych intencjach, ale zbyt ogólne.

Nulls nie są złe. Nie są antypatternem. Nie należy ich za wszelką cenę unikać. Jednak wartości null są podatne na błędy (od braku sprawdzenia wartości null).

Ale nie rozumiem, w jaki sposób brak sprawdzenia wartości null jest dowodem, że użycie wartości null jest z natury niewłaściwe.

Jeśli wartość null jest zła, to również indeksy tablic i wskaźniki C ++. Oni nie są. Łatwo jest strzelać sobie w stopę, ale nie należy ich unikać za wszelką cenę.

W pozostałej części tej odpowiedzi zamierzam dostosować zamiar „zerowe są złe” do bardziej dopracowanych „ zerowych należy traktować odpowiedzialnie ”.


Twoje rozwiązanie

Chociaż zgadzam się z twoją opinią na temat stosowania wartości null jako reguły globalnej; W tym konkretnym przypadku nie zgadzam się z użyciem przez Ciebie wartości null.

Podsumowując poniższe opinie: nie rozumiesz celu dziedziczenia i polimorfizmu. Podczas gdy twój argument, aby użyć null, jest ważny, twój dalszy „dowód”, dlaczego drugi sposób jest zły, opiera się na niewłaściwym wykorzystaniu dziedziczenia, co czyni twoje punkty nieco nieistotnymi dla problemu zerowego.

Większość aktualizacji wiąże się oczywiście z graczem - w końcu trzeba wiedzieć, który gracz wykonał ruch w tej turze. Jednak niektóre aktualizacje nie mogą mieć z nimi istotnego powiązania.

Jeśli te aktualizacje są znacząco różne (którymi są), potrzebne są dwa osobne typy aktualizacji.

Weźmy na przykład wiadomości. Masz komunikaty o błędach i wiadomości czatu IM. Ale chociaż mogą zawierać bardzo podobne dane (komunikat w postaci ciągu i nadawcę), nie zachowują się w ten sam sposób. Powinny być używane osobno, jako różne klasy.

To, co teraz robisz, polega na rozróżnieniu dwóch typów aktualizacji na podstawie nieważności właściwości Player. Jest to równoważne z podejmowaniem decyzji między komunikatem IM a komunikatem o błędzie na podstawie istnienia kodu błędu. Działa, ale nie jest to najlepsze podejście.

  • Kod JS będzie teraz po prostu odbierał obiekt bez Player jako zdefiniowaną właściwość, która jest taka sama, jakby była pusta, ponieważ oba przypadki wymagają sprawdzenia, czy wartość jest obecna, czy nie;

Twój argument opiera się na wadach polimorfizmu. Jeśli masz metodę, która zwraca GameUpdateobiekt, generalnie nie powinno się nigdy starać rozróżniać GameUpdate, PlayerGameUpdatelub NewlyDevelopedGameUpdate.

Klasa podstawowa musi mieć w pełni działającą umowę, tzn. Jej właściwości są zdefiniowane w klasie podstawowej, a nie w klasie pochodnej (to powiedziawszy, wartość tych właściwości podstawowych można oczywiście zastąpić w klasach pochodnych).

To sprawia, że ​​twój argument jest dyskusyjny. W dobrej praktyce nigdy nie powinieneś się przejmować, że twój GameUpdateobiekt ma lub nie ma gracza. Jeśli próbujesz uzyskać dostęp do Playerwłaściwości a GameUpdate, robisz coś nielogicznego.

Wpisane języki, takie jak C #, nawet nie pozwolą ci uzyskać dostępu do Playerwłaściwości, GameUpdateponieważ GameUpdatepo prostu nie ma tej właściwości. JavaScript jest znacznie bardziej luźny w swoim podejściu (i nie wymaga wstępnej kompilacji), więc nie zadaje sobie trudu, aby cię poprawić i zamiast tego powoduje wybuch w czasie wykonywania.

  • Gdyby do klasy GameUpdate dodano kolejne pole zerowalne, musiałbym mieć możliwość wielokrotnego dziedziczenia, aby kontynuować to projektowanie - ale MI jest samo w sobie złe (według bardziej doświadczonych programistów) i, co ważniejsze, C # nie mam, więc nie mogę go użyć.

Nie powinieneś tworzyć klas opartych na dodawaniu właściwości zerowalnych. Powinieneś tworzyć klasy w oparciu o funkcjonalnie unikalne typy aktualizacji gier .


Jak w ogóle uniknąć problemów z zerowym?

Uważam, że warto opracować sposób, w jaki można w sposób znaczący wyrazić brak wartości. Jest to zbiór rozwiązań (lub złych podejść) w dowolnej kolejności.

1. W każdym razie użyj null.

To jest najłatwiejsze podejście. Jednak rejestrujesz się, aby uzyskać mnóstwo kontroli zerowych i (gdy tego nie zrobisz) rozwiązywanie problemów z wyjątkami referencji zerowych.

2. Użyj niepustej wartości bez znaczenia

Prostym przykładem jest tutaj indexOf():

"abcde".indexOf("b"); // 1
"abcde".indexOf("f"); // -1

Zamiast tego nulldostajesz -1. Na poziomie technicznym jest to poprawna wartość całkowita. Jednak wartość ujemna to nonsensowna odpowiedź, ponieważ oczekuje się, że indeksy będą dodatnimi liczbami całkowitymi.

Logicznie można tego użyć tylko w przypadkach, w których typ zwracany pozwala na więcej możliwych wartości, niż można je w sposób znaczący zwrócić (indexOf = liczby całkowite dodatnie; int = liczby całkowite ujemne i dodatnie)

W przypadku typów referencyjnych nadal można zastosować tę zasadę. Na przykład, jeśli masz encję o identyfikatorze całkowitym, możesz zwrócić fikcyjny obiekt o jego identyfikatorze ustawionym na -1, w przeciwieństwie do wartości null.
Musisz po prostu zdefiniować „stan obojętny”, za pomocą którego można zmierzyć, czy obiekt jest rzeczywiście użyteczny, czy nie.

Zauważ, że nie powinieneś polegać na ustalonych wcześniej wartościach ciągów, jeśli nie kontrolujesz wartości ciągów. Możesz rozważyć ustawienie nazwy użytkownika na „null”, gdy chcesz zwrócić pusty obiekt, ale możesz napotkać problemy, gdy Christopher Null zarejestruje się w Twojej usłudze .

3. Zamiast tego rzuć wyjątek.

Nie, po prostu nie. Wyjątki nie powinny być obsługiwane dla przepływu logicznego.

To powiedziawszy, istnieją pewne przypadki, w których nie chcesz ukryć wyjątku (zerowego odniesienia), ale raczej pokazać odpowiedni wyjątek. Na przykład:

var existingPerson = personRepository.GetById(123);

Może to rzucić PersonNotFoundException(lub podobny), gdy nie ma osoby o identyfikatorze 123.

W pewien sposób jest to dopuszczalne tylko w przypadkach, w których nie znalezienie przedmiotu uniemożliwia dalsze przetwarzanie. Jeśli nie można znaleźć przedmiotu, a aplikacja może być kontynuowana, NIGDY nie należy rzucać wyjątku . Nie mogę tego wystarczająco podkreślić.


5

Przyczyną, dla której wartości null są złe, nie jest to, że brakująca wartość jest kodowana jako null, ale że każda wartość odniesienia może być null. Jeśli masz C #, prawdopodobnie i tak się zgubiłeś. Nie widzę sensu, żeby używać Opcjonalnego, ponieważ typ odniesienia jest już opcjonalny. Ale w Javie są adnotacje @Notnull, które wydaje się być w stanie skonfigurować na poziomie pakietu , a następnie w przypadku wartości, których można pominąć, można użyć adnotacji @Nullable.


Tak! Problem jest niedorzecznym domyślnym.
Deduplicator

Nie zgadzam się. Programowanie w stylu, który unika null, prowadzi do tego, że mniej zmiennych referencyjnych ma wartość null, co prowadzi do mniejszej liczby zmiennych referencyjnych, które nie powinny mieć wartości null. To nie jest 100%, ale może 90%, co jest warte IMO.
Przywróć Monikę

1

Wartości zerowe nie są złe, realistycznie nie ma sposobu na implementację żadnej z alternatyw, jeśli w pewnym momencie nie zajmiemy się czymś, co wygląda jak zero.

Jednak wartości zerowe są niebezpieczne . Podobnie jak w przypadku innych konstruktów niskiego poziomu, jeśli popełnisz błąd, nie ma nic, co mogłoby cię złapać.

Myślę, że istnieje wiele ważnych argumentów przeciwko wartości null:

  • Że jeśli jesteś już w domenie wysokiego poziomu, istnieją bezpieczniejsze wzorce niż przy użyciu modelu niskiego poziomu.

  • Jeśli nie potrzebujesz zalet systemu niskiego poziomu i nie jesteś przygotowany na odpowiednią ostrożność, być może nie powinieneś używać języka niskiego poziomu.

  • itp ...

Wszystkie są ważne i ponadto nie trzeba się starać pisać autorów i seterów itp. Jest to powszechny powód, aby nie stosować się do tej rady. Nic dziwnego, że wielu programistów doszło do wniosku, że wartości zerowe są niebezpieczne i leniwe (zła kombinacja!), Ale podobnie jak wiele innych „uniwersalnych” porad, nie są tak uniwersalne, jak się je wypowiada.

Dobrzy ludzie, którzy napisali ten język, myśleli, że będą dla kogoś przydatni. Jeśli rozumiesz zastrzeżenia i nadal uważasz, że są one odpowiednie dla Ciebie, nikt inny nie będzie wiedział nic lepszego.


Chodzi o to, że „uniwersalna rada” zwykle nie jest sformułowana jako „uniwersalnie”, jak twierdzą ludzie. Jeśli znajdziesz oryginalne źródło takich porad, zwykle mówią o zastrzeżeniach, o których wiedzą doświadczeni programiści. To, co się dzieje, polega na tym, że gdy ktoś czyta radę, ignoruje te zastrzeżenia i zapamiętuje tylko część „uniwersalna rada”, więc kiedy odnoszą się do innych, okazuje się, że jest ona bardziej „uniwersalna” niż zamierzał autor .
Nicol Bolas

1
@NicolBolas, masz rację, ale oryginalne źródło nie jest wskazówką, którą większość ludzi cytuje, to przekazana wiadomość. Chciałem tylko powiedzieć, że wielu programistom powiedziano: „nigdy nie rób X, X jest zły / zły / cokolwiek” i dokładnie tak, jak mówisz, brakuje mu wielu zastrzeżeń. Może zostały upuszczone, może nigdy nie były obecne, efekt końcowy jest taki sam.
drjpizzle

0

Java ma Optional klasę z jawną ifPresent+ lambda do bezpiecznego dostępu do dowolnej wartości. Można to również zrobić w JS:

Widzieć to pytanie StackOverflow .

Nawiasem mówiąc: wielu purystów uzna to użycie za wątpliwe, ale nie sądzę. Istnieją funkcje opcjonalne.


Nie może być też odniesienie do java.lang.Optionalbycia null?
Deduplicator

@Deduplicator Nie, rzuciłby Optional.of(null)wyjątek, Optional.ofNullable(null)dałby Optional.empty()- wartość nieobecna .
Joop Eggen

I Optional<Type> a = null?
Deduplicator

@Deduplicator prawda, ale tam powinieneś użyć Optional<Optional<Type>> a = Optional.empty();;) - Innymi słowy w JS (i innych językach) takie rzeczy nie będą możliwe. Zrobiłaby to Scala z wartościami domyślnymi. Java może z enum. JS wątpię: Optional<Type> a = Optional.empty();.
Joop Eggen

Więc przenieśliśmy się z jednej pośredniej i potencjalnej nulllub pustej, do dwóch plus kilka dodatkowych metod na umieszczonym obiekcie. To całkiem spore osiągnięcie.
Deduplicator

0

CAR Hoare nazwał nulls swoim „błędem miliarda dolarów”. Należy jednak zauważyć, że myślał, że rozwiązaniem nie jest „brak null nigdzie”, ale „domyślne wartości zerowe, a null tylko tam, gdzie są one semantycznie wymagane”.

Problem z wartością null w językach, które powtarzają swój błąd, polega na tym, że nie można powiedzieć, że funkcja oczekuje danych, które nie mają wartości null; jest zasadniczo niewrażliwy na język. Zmusza to funkcje do włączenia wielu niepotrzebnych zewnętrznych płyt kotłowych zajmujących się zerowaniem, a zapominanie o sprawdzeniu wartości zerowej jest częstym źródłem błędów.

Aby zhakować ten podstawowy brak ekspresji, niektóre społeczności (np. Społeczność Scala) nie używają wartości zerowej. W Scali, jeśli chcesz null wartości typu T, bierzesz argument typu Option[T], a jeśli chcesz wartość null, po prostu bierzesz T. Jeśli ktoś poda zero w twoim kodzie, Twój kod wysadzi się w powietrze. Uważa się jednak, że kod wywołujący jest błędnym kodem. Optionjest całkiem dobrym zamiennikiem wartości null, ponieważ typowe wzorce obsługi wartości null są wyodrębniane do funkcji bibliotecznych takich jak .map(f)lub .getOrElse(someDefault).

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.