Uznanie autorstwa: Wynikało to z powiązanego pytania P.SE
Moje doświadczenie jest w C / C ++, ale pracowałem sporo w Javie i obecnie koduję C #. Ze względu na moje tło C sprawdzanie wskaźników zakończonych i zwróconych jest używane, ale potwierdzam, że podważam mój punkt widzenia.
Niedawno widziałem wzmiankę o zerowym wzorzec obiektu, w którym chodzi o to, że obiekt jest zawsze zwracany. Przypadek normalny zwraca oczekiwany, zapełniony obiekt, a przypadek błędu zwraca pusty obiekt zamiast wskaźnika zerowego. Założeniem jest to, że funkcja wywołująca zawsze będzie miała jakiś obiekt, aby uzyskać do niego dostęp, a tym samym uniknąć naruszenia pamięci o zerowym dostępie.
- Więc jakie są zalety / wady sprawdzania wartości zerowej w porównaniu do używania wzorca obiektu zerowego?
Widzę czystszy kod wywołujący za pomocą NOP, ale mogę również zobaczyć, gdzie stworzyłoby ukryte awarie, które w przeciwnym razie nie byłyby podnoszone. Wolę, aby moja aplikacja mocno zawiodła (inaczej wyjątek) podczas jej opracowywania, niż cicha pomyłka w ucieczce na wolność.
- Czy wzorzec obiektu zerowego nie może mieć podobnych problemów, jak brak sprawdzania wartości zerowej?
Wiele obiektów, nad którymi pracowałem, zawierało własne obiekty lub pojemniki. Wygląda na to, że musiałbym mieć specjalny przypadek, aby zagwarantować, że wszystkie kontenery głównego obiektu mają własne puste obiekty. Wygląda na to, że może to być brzydkie z wieloma warstwami zagnieżdżania.
źródło
Odpowiedzi:
Nie należy używać Wzorca Null Object w miejscach, w których zwracany jest Null (lub Null Object), ponieważ wystąpiła katastrofalna awaria. W tych miejscach nadal zwracałbym zero. W niektórych przypadkach, jeśli nie ma odzyskiwania, równie dobrze możesz ulec awarii, ponieważ przynajmniej zrzut awaryjny wskaże dokładnie, gdzie wystąpił problem. W takich przypadkach, gdy dodasz własną obsługę błędów, nadal będziesz zabijać proces (znowu mówiłem o przypadkach, w których nie ma odzyskiwania), ale obsługa błędów będzie maskować bardzo ważne informacje, które dostarczyłby zrzut awaryjny.
Wzorzec zerowy obiektu jest bardziej odpowiedni dla miejsc, w których istnieje domyślne zachowanie, które można podjąć w przypadku, gdy obiekt nie zostanie znaleziony. Na przykład rozważ następujące kwestie:
Jeśli używasz NOP, napiszesz:
Zauważ, że zachowanie tego kodu brzmi „jeśli Bob istnieje, chcę zaktualizować jego adres”. Oczywiście, jeśli twoja aplikacja wymaga obecności Boba, nie chcesz po cichu odnieść sukcesu. Ale są przypadki, w których tego rodzaju zachowanie byłoby odpowiednie. A w takich przypadkach, czy NOP nie tworzy znacznie bardziej przejrzystego i zwięzłego kodu?
W miejscach, w których tak naprawdę nie można żyć bez Boba, chciałbym, aby GetUser () wyrzucił wyjątek aplikacji (tj. Brak naruszenia praw dostępu itp.), Który byłby obsługiwany na wyższym poziomie i zgłaszałby ogólną awarię operacji. W tym przypadku nie ma potrzeby NOP, ale nie ma też potrzeby jawnego sprawdzania NULL. IMO, sprawdzanie NULL, tylko powiększa kod i odbiera czytelność. Sprawdź, czy NULL jest nadal właściwym wyborem dla niektórych interfejsów, ale nie tak wiele, jak niektórzy ludzie myślą.
źródło
Plusy
Cons
Ten ostatni „pro” jest głównym wyróżnikiem (z mojego doświadczenia), kiedy należy zastosować każdy z nich. „Czy awaria powinna być głośna?”. W niektórych przypadkach chcesz, aby niepowodzenie było trudne i natychmiastowe; jeśli jakiś scenariusz, który nigdy nie powinien się wydarzyć, tak się dzieje. Jeśli nie znaleziono istotnego zasobu ... itd. W niektórych przypadkach nie masz nic przeciwko rozsądnemu domyślnemu ustawieniu, ponieważ tak naprawdę nie jest to błąd : uzyskanie wartości ze słownika, ale na przykład brakuje klucza.
Jak każda inna decyzja projektowa, istnieją wady i zalety w zależności od potrzeb.
źródło
Nie jestem dobrym źródłem obiektywnych informacji, ale subiektywnie:
W środowisku takim jak Java lub C # nie zapewni to głównej korzyści z jawności - bezpieczeństwo rozwiązania jest kompletne, ponieważ nadal możesz otrzymać wartość null w miejscu dowolnego obiektu w systemie, a Java nie ma żadnych środków (z wyjątkiem niestandardowych adnotacji dla Beans itp.), aby cię przed tym chronić, z wyjątkiem jawnych ifs. Ale w systemie wolnym od implementacji zerowej podejście do problemu w taki sposób (z obiektami reprezentującymi wyjątkowe przypadki) daje wszystkie wymienione korzyści. Więc spróbuj przeczytać o czymś innym niż języki głównego nurtu, a odkryjesz, że zmieniasz sposoby kodowania.
Ogólnie rzecz biorąc, obiekty zerowe / typy błędów / zwracane typy są uważane za bardzo solidną strategię obsługi błędów i dość matematycznie uzasadnioną, podczas gdy wyjątki dotyczące strategii Java lub C idą jak tylko jeden krok powyżej strategii „die ASAP” - więc w zasadzie zostawić cały ciężar niestabilność i nieprzewidywalność systemu dla dewelopera lub opiekuna.
Istnieją również monady, które można uznać za lepsze od tej strategii (początkowo także o wyższym stopniu złożoności zrozumienia), ale chodzi o przypadki, w których trzeba modelować zewnętrzne aspekty typu, podczas gdy Null Object modeluje aspekty wewnętrzne. Więc NonExistingUser jest prawdopodobnie lepiej modelowany jako monad może_istniejący.
I na koniec, nie sądzę, że istnieje związek między wzorcem Null Object a cichą obsługą błędów. Powinno być na odwrót - powinno zmusić cię do jawnego modelowania każdego przypadku błędu i odpowiednio go obsłużyć. Teraz oczywiście możesz zdecydować się być niechlujny (z jakiegokolwiek powodu) i pominąć jawne rozpatrywanie przypadku błędu, ale załatw go ogólnie - cóż, to był twój wyraźny wybór.
źródło
Myślę, że warto zauważyć, że żywotność obiektu zerowego wydaje się nieco inna w zależności od tego, czy Twój język jest wpisany statycznie. Ruby to język, który implementuje obiekt dla Null (lub
Nil
w terminologii tego języka).Zamiast zwracać wskaźnik do niezainicjowanej pamięci, język zwróci obiekt
Nil
. Ułatwia to fakt, że język jest dynamicznie wpisywany. Funkcja nie gwarantuje, że zawsze zwróci anint
. Może wrócić,Nil
jeśli tego właśnie potrzebuje. Jest to bardziej skomplikowane i trudne do zrobienia w języku o typie statycznym, ponieważ naturalnie oczekujesz, że wartość null będzie miała zastosowanie do każdego obiektu, który można wywołać przez odwołanie. Będziesz musiał rozpocząć wdrażanie zerowych wersji dowolnego obiektu, który może być zerowy (lub pójść drogą Scala / Haskell i mieć jakieś opakowanie „Może”).MrLister poruszył fakt, że w C ++ nie ma gwarancji, że będziesz mieć inicjalizowaną referencję / wskaźnik podczas jego pierwszego tworzenia. Dysponując dedykowanym obiektem zerowym zamiast surowego wskaźnika zerowego, można uzyskać dodatkowe fajne funkcje. Ruby
Nil
zawiera funkcjęto_s
(toString), która daje pusty tekst. Jest to świetne, jeśli nie masz nic przeciwko otrzymaniu zerowego zwrotu na łańcuchu i chcesz pominąć zgłaszanie go bez awarii. Klasy otwarte pozwalają również ręcznie zastąpić dane wyjściowe wNil
razie potrzeby.To prawda, że można to wszystko wziąć z odrobiną soli. Wierzę, że w języku dynamicznym obiekt zerowy może być wielką wygodą, jeśli jest używany poprawnie. Niestety pełne wykorzystanie jego możliwości może wymagać edycji podstawowej funkcji, co może utrudnić zrozumienie i utrzymanie programu przez osoby przyzwyczajone do pracy z bardziej standardowymi formularzami.
źródło
Aby uzyskać dodatkowy punkt widzenia, spójrz na Cel C, gdzie zamiast posiadania obiektu Null wartość zero działa jak obiekt. Każda wiadomość wysłana do obiektu zerowego nie ma żadnego efektu i zwraca wartość 0 / 0,0 / NO (false) / NULL / zero, niezależnie od typu zwracanej wartości metody. Jest to używane jako bardzo wszechobecny wzorzec w programowaniu MacOS X i iOS, i zwykle metody są zaprojektowane w taki sposób, że zerowy obiekt zwracający 0 jest rozsądnym wynikiem.
Na przykład, jeśli chcesz sprawdzić, czy łańcuch ma jeden lub więcej znaków, możesz napisać
i uzyskaj rozsądny wynik, jeśli aString == zero, ponieważ nieistniejący ciąg nie zawiera żadnych znaków. Ludzie zwykle przyzwyczajają się do tego, ale prawdopodobnie zajęłoby mi trochę czasu, aby przyzwyczaić się do sprawdzania wartości zerowych w innych językach.
(Jest też singletonowy obiekt klasy NSNull, który zachowuje się zupełnie inaczej i nie jest zbyt często wykorzystywany w praktyce).
źródło
nil
dostać się#define
do NULL i zachowuje się jak obiekt zerowy. Jest to funkcja środowiska wykonawczego, podczas gdy w zerowych wskaźnikach języka C # / Java emitowane są błędy.Nie używaj, jeśli możesz tego uniknąć. Użyj funkcji fabrycznej zwracającej typ opcji, krotkę lub dedykowany typ sumy. Innymi słowy, reprezentują potencjalnie niewykonaną wartość z innym typem niż wartość gwarantowana, że nie zostanie niewykonana.
Najpierw wypiszmy desiderata, a następnie zastanówmy się, jak możemy to wyrazić w kilku językach (C ++, OCaml, Python)
grep
szukanie potencjalnych błędów.Myślę, że napięcie pomiędzy zerowym wzorcem obiektu a zerowymi wskaźnikami pochodzi z (5). Jeśli wykryjemy błędy wystarczająco wcześnie, (5) stanie się dyskusyjne.
Rozważmy ten język według języka:
C ++
Moim zdaniem klasa C ++ powinna generalnie być domyślnie konstruowana, ponieważ ułatwia interakcje z bibliotekami i ułatwia korzystanie z tej klasy w kontenerach. Upraszcza także dziedziczenie, ponieważ nie trzeba myśleć o tym, który konstruktor nadklasy ma zostać wywołany.
Oznacza to jednak, że nie możesz mieć pewności, czy wartość typu
MyClass
jest w stanie domyślnym, czy nie. Oprócz wprowadzenia wbool nonempty
polu wykonawczym podobnego lub podobnego pola do domyślnej powierzchni, najlepszym, co możesz zrobić, jest tworzenie nowych instancjiMyClass
w sposób, który zmusza użytkownika do sprawdzenia .Polecam przy użyciu funkcji fabryki, która zwraca
std::optional<MyClass>
,std::pair<bool, MyClass>
lub odwołania R-wartość nastd::unique_ptr<MyClass>
ile to możliwe.Jeśli chcesz, aby funkcja fabryczna zwróciła jakiś „symbol zastępczy”, który różni się od skonstruowanej domyślnie
MyClass
, użyj astd::pair
i upewnij się, że dokumentuje to, że twoja funkcja to robi.Jeśli funkcja fabryczna ma unikalną nazwę, łatwo jest
grep
ją nazwać i poszukać niechlujstwa. Jednak trudno jestgrep
w przypadkach, w których programista powinien był użyć funkcji fabrycznej, ale nie zrobił tego .OCaml
Jeśli używasz języka takiego jak OCaml, możesz po prostu użyć
option
typu (w standardowej bibliotece OCaml) lubeither
typu (nie w standardowej bibliotece, ale łatwo ją rozwinąć). Lubdefaultable
typ (tworzę termindefaultable
).Jak
defaultable
pokazano powyżej, jest lepszy niż para, ponieważ użytkownik musi wyodrębnić dopasowanie wzorca, aby wyodrębnić'a
i nie może po prostu zignorować pierwszego elementu pary.Ekwiwalent
defaultable
typu pokazanego powyżej może być używany w C ++ przy użyciu astd::variant
z dwoma instancjami tego samego typu, ale częścistd::variant
interfejsu API są bezużyteczne, jeśli oba typy są takie same. Jest to również dziwne zastosowanie,std::variant
ponieważ jego konstruktory typów są bezimienne.Pyton
Zresztą nie dostajesz żadnego sprawdzania czasu kompilacji dla Pythona. Jednak dynamiczne pisanie nie powoduje, że do umieszczenia kompilatora potrzebna jest instancja zastępcza dowolnego typu.
Polecam po prostu zgłoszenie wyjątku, gdy będziesz zmuszony utworzyć domyślną instancję.
Jeśli nie jest to do zaakceptowania, zaleciłbym utworzenie
DefaultedMyClass
odziedziczonegoMyClass
i zwrócenie go z funkcji fabrycznej. Dzięki temu zyskujesz elastyczność w zakresie „eliminowania” funkcji w domyślnych instancjach, jeśli zajdzie taka potrzeba.źródło