Czy nowe pole boolowskie jest lepsze niż odwołanie zerowe, gdy wartość może być znacząco nieobecna?

39

Załóżmy na przykład, że mam klasę Member, która ma lastChangePasswordTime:

class Member{
  .
  .
  .
  constructor(){
    this.lastChangePasswordTime=null,
  }
}

którego lastChangePasswordTime może być nieobecny, ponieważ niektórzy członkowie mogą nigdy nie zmieniać swoich haseł.

Ale zgodnie z Jeśli wartości zerowe są złe, co należy zastosować, gdy wartość może być znacząco nieobecna? i https://softwareengineering.stackexchange.com/a/12836/248528 , nie powinienem używać wartości null do reprezentowania istotnie nieobecnej wartości. Próbuję więc dodać flagę logiczną:

class Member{
  .
  .
  .
  constructor(){
    this.isPasswordChanged=false,
    this.lastChangePasswordTime=null,
  }
}

Ale myślę, że jest dość przestarzały, ponieważ:

  1. Gdy isPasswordChanged ma wartość false, lastChangePasswordTime musi mieć wartość null, a sprawdzenie lastChangePasswordTime == null jest prawie identyczne jak sprawdzenie isPasswordChanged ma wartość false, więc wolę sprawdzić lastChangePasswordTime == null bezpośrednio

  2. Zmieniając tutaj logikę, mogę zapomnieć o aktualizacji obu pól.

Uwaga: gdy użytkownik zmienia hasło, zapisuje czas w następujący sposób:

this.lastChangePasswordTime=Date.now();

Czy dodatkowe pole boolowskie jest lepsze niż odwołanie zerowe?

ocomfd
źródło
51
To pytanie jest specyficzne dla języka, ponieważ to, co stanowi dobre rozwiązanie, zależy w dużej mierze od tego, co oferuje Twój język programowania. Na przykład w C ++ 17 lub Scali użyłbyś std::optionallub Option. W innych językach może być konieczne zbudowanie odpowiedniego mechanizmu lub skorzystanie z nullczegoś podobnego, ponieważ jest to bardziej idiomatyczne.
Christian Hackl
16
Czy istnieje powód, dla którego nie chcesz, aby lastChangePasswordTime ustawiono na czas tworzenia hasła (w końcu tworzenie jest modyfikacją)?
Kristian H
@ChristianHackl hmm, zgadzam się, że istnieją różne „idealne” rozwiązania, ale nie widzę żadnego (większego) języka, chociaż użycie oddzielnej wartości logicznej byłoby ogólnie lepszym pomysłem niż sprawdzanie wartości zerowej / zerowej. Jednak nie jestem całkowicie pewien C / C ++, ponieważ nie byłem tam aktywny od dłuższego czasu.
Frank Hopkins
@FrankHopkins: Przykładem mogą być języki, w których zmienne można pozostawić niezainicjowane, np. C lub C ++. lastChangePasswordTimemoże być tam niezainicjowanym wskaźnikiem, a porównywanie go do czegokolwiek byłoby zachowaniem niezdefiniowanym. Nie jest to naprawdę ważny powód, aby nie inicjować wskaźnika do NULL/ nullptrzamiast, szczególnie nie we współczesnym C ++ (gdzie w ogóle nie używałbyś wskaźnika), ale kto wie? Innym przykładem mogą być języki bez wskaźników lub złą obsługą wskaźników. (Przychodzi na myśl FORTRAN 77 ...)
Christian Hackl
Użyłbym do tego wyliczenia z 3 przypadkami :)
J. Doe

Odpowiedzi:

72

Nie rozumiem, dlaczego, jeśli masz wyraźnie nieobecną wartość, nullnie należy jej używać, jeśli jesteś rozmyślny i ostrożny.

Jeśli Twoim celem jest otoczenie wartości zerowalnej, aby zapobiec jej przypadkowemu odwołaniu, sugerowałbym utworzenie isPasswordChangedwartości jako funkcji lub właściwości, która zwraca wynik kontroli zerowej, na przykład:

class Member {
    DateTime lastChangePasswordTime = null;
    bool isPasswordChanged() { return lastChangePasswordTime != null; }

}

Moim zdaniem robienie tego w ten sposób:

  • Daje lepszą czytelność kodu niż sprawdzanie wartości zerowej, co może utracić kontekst.
  • Eliminuje potrzebę martwienia się o utrzymanie wspomnianej isPasswordChangedwartości.

Sposób przechowywania danych (przypuszczalnie w bazie danych) byłby odpowiedzialny za zapewnienie zachowania wartości zerowych.

TZHX
źródło
29
+1 za otwarcie. Bezmyślne użycie wartości null jest generalnie odrzucane tylko w przypadkach, gdy null jest nieoczekiwanym wynikiem, tj. Gdy nie miałoby to sensu (dla konsumenta). Jeśli wartość null ma znaczenie, nie jest to nieoczekiwany wynik.
Flater
28
Powiedziałbym to trochę mocniej, ponieważ nigdy nie ma dwóch zmiennych, które utrzymują ten sam stan. To zawiedzie. Ukrywanie wartości zerowej, zakładanie, że wartość zerowa ma sens wewnątrz instancji, z zewnątrz jest bardzo dobrą rzeczą. W takim przypadku możesz później zdecydować, że 1970-01-01 oznacza, że ​​hasło nigdy nie zostało zmienione. Wtedy logika reszty programu nie musi się tym przejmować.
Wygięty
3
Możesz zapomnieć zadzwonić, isPasswordChangedtak jak możesz zapomnieć, aby sprawdzić, czy jest zerowy. Nic tu nie zyskałem.
Gherman
9
@Gherman Jeśli konsument nie rozumie, że wartość null jest znacząca lub że musi sprawdzić, czy wartość jest obecna, jest on zgubiony w obu kierunkach, ale jeśli zastosowana zostanie metoda, wszyscy będą czytać kod, dlaczego jest czekiem i istnieje uzasadniona szansa, że ​​wartość jest zerowa / nie występuje. W przeciwnym razie nie jest jasne, czy był to tylko programista, który dodaje kontrolę zerową tylko „bo dlaczego nie” lub czy jest to część koncepcji. Jasne, można się dowiedzieć, ale jeden sposób, w jaki masz informacje bezpośrednio, a drugi, aby zajrzeć do implementacji.
Frank Hopkins
2
@FrankHopkins: Dobra uwaga, ale jeśli używana jest współbieżność, nawet kontrola pojedynczej zmiennej może wymagać zablokowania.
Christian Hackl
63

nullsnie są złe. Używanie ich bez myślenia jest. Jest to przypadek, w którym nulljest poprawna odpowiedź - nie ma daty.

Pamiętaj, że twoje rozwiązanie stwarza więcej problemów. Jakie jest znaczenie ustawienia daty na coś, ale isPasswordChangedjest to fałsz? Właśnie stworzyłeś przypadek sprzecznych informacji, które musisz specjalnie przechwycić i potraktować, podczas gdy nullwartość ma jasno określone, jednoznaczne znaczenie i nie może być sprzeczna z innymi informacjami.

Więc nie, twoje rozwiązanie nie jest lepsze. Przyjęcie nullwartości jest tutaj właściwym podejściem. Ludzie, którzy twierdzą, że nullzawsze jest zły, bez względu na kontekst, nie rozumieją, dlaczego nullistnieje.

Tomek
źródło
17
Historycznie nullistnieje, ponieważ Tony Hoare popełnił błąd o wartości miliarda dolarów w 1965 roku. Ludzie, którzy twierdzą, że nullto „zło”, nadmiernie upraszczają ten temat, ale dobrze, że współczesne języki lub style programowania odchodzą null, zastępując z dedykowanymi typami opcji.
Christian Hackl
9
@ChristianHackl: po prostu poza historycznym zainteresowaniem - die Hoare kiedykolwiek powiedział, jaka byłaby lepsza praktyczna alternatywa (bez przejścia na zupełnie nowy paradygmat)?
AnoE
5
@AnoE: Interesujące pytanie. Nie wiem o tym, co Hoare miał do powiedzenia na ten temat, ale programowanie bez wartości zerowej jest często rzeczywistością w nowoczesnych, dobrze zaprojektowanych językach programowania.
Christian Hackl
10
@Tom wat? W językach takich jak C # i Java DateTimepole jest wskaźnikiem. Cała koncepcja nullma sens tylko dla wskaźników!
Todd Sewell
16
@Tom: Wskaźniki zerowe są niebezpieczne tylko w przypadku języków i implementacji, które umożliwiają ich ślepą indeksację i dereferencję, bez względu na to, co może być zabawne. W języku, który przechwytuje próby indeksowania lub wyzerowania wskaźnika zerowego, często będzie mniej niebezpieczny niż wskaźnik do obojętnego obiektu. Istnieje wiele sytuacji, w których nienormalne zakończenie programu może być lepsze niż generowanie liczb bez znaczenia, a w systemach, które je pułapkują, zerowe wskaźniki są właściwą receptą na to.
supercat
29

W zależności od języka programowania mogą istnieć dobre alternatywy, takie jak optionaltyp danych. W C ++ (17) byłoby to std :: opcjonalne . Może być nieobecny lub mieć dowolną wartość bazowego typu danych.

dasmy
źródło
8
Jest też Optionalw java; znaczenie zerowej Optionalzależy od ciebie ...
Matthieu M.
1
Przyszedł tutaj, aby połączyć się również z Opcjonalnym. Kodowałbym to jako typ w języku, który je miał.
Zipp
2
@FrankHopkins Szkoda, że ​​nic w Javie nie powstrzymuje cię przed pisaniem Optional<Date> lastPasswordChangeTime = null;, ale nie poddałbym się tylko z tego powodu. Zamiast tego zaszczepiłbym bardzo mocno trzymaną zasadę zespołu, że nigdy nie można przypisywać żadnych wartości opcjonalnych null. Opcjonalnie kupuje zbyt wiele fajnych funkcji, aby łatwo z nich zrezygnować. W prosty sposób można przypisać wartości domyślne ( orElselub leniwe oceny: orElseGet), błędy throw ( orElseThrow) przekształca wartości w sposób bezpieczny zerowej ( map, flatmap), itd.
Alexander
5
@FrankHopkins Checking isPresentjest, moim zdaniem, prawie zawsze zapachem kodu i dość wiarygodnym znakiem, że twórcy używający tych opcji nie mają racji . Rzeczywiście, w zasadzie nie daje to żadnych korzyści ref == null, ale nie jest to również coś, czego powinieneś często używać. Nawet jeśli zespół jest niestabilny, narzędzie do analizy statycznej może ustanowić prawo, takie jak linijka lub FindBugs , które mogą wychwycić większość przypadków.
Alexander
5
@FrankHopkins: Być może społeczność Java potrzebuje jedynie niewielkiej zmiany paradygmatu, co może potrwać jeszcze wiele lat. Jeśli spojrzeć na Scala, na przykład, obsługuje null, ponieważ język jest w 100% kompatybilny z Java, ale nikt nie używa go, i Option[T]to wszędzie, z doskonałą obsługą funkcjonalnego programowania jak map, flatMap, dopasowywania wzorców i tak dalej, który jedzie daleko, daleko poza == nullkontrolami. Masz rację, że teoretycznie typ opcji brzmi jak chwalebny wskaźnik, ale rzeczywistość dowodzi, że ten argument jest błędny.
Christian Hackl
11

Poprawnie używając null

Istnieją różne sposoby korzystania null. Najczęstszym i semantycznie poprawnym sposobem jest użycie go, gdy możesz mieć lub nie mieć jednej wartości. W tym przypadku wartość jest równa nulllub jest czymś znaczącym, jak zapis z bazy danych lub coś takiego.

W takich sytuacjach najczęściej używasz go w następujący sposób (w pseudokodzie):

if (value is null) {
  doSomethingAboutIt();
  return;
}

doSomethingUseful(value);

Problem

I ma bardzo duży problem. Problem polega na tym, że do czasu wywołania doSomethingUsefulwartość mogła nie zostać sprawdzona null! Jeśli tak nie było, program najprawdopodobniej ulegnie awarii. A użytkownik może nawet nie zobaczyć żadnych dobrych komunikatów o błędach, pozostawiając coś w rodzaju „strasznego błędu: pożądana wartość, ale dostała zero!” (po aktualizacji: chociaż może występować jeszcze mniej informacji, takich jak Segmentation fault. Core dumped.lub, co gorsza, brak błędów i nieprawidłowe manipulowanie wartością NULL w niektórych przypadkach)

Niezwykle częstym błędem jest zapominanie o pisaniu czeków nulli radzeniu sobie w nullsytuacjach . Właśnie dlatego Tony Hoare, który wynalazł, powiedział podczas konferencji programowej QCon London w 2009 roku, że popełnił błąd miliarda dolarów w 1965 roku: https://www.infoq.com/presentations/Null-References-The-Billion-Dollar- Błąd-Tony-Hoarenull

Unikanie problemu

Niektóre technologie i języki sprawiają, że sprawdzanie pod kątem nullniemożności zapomnienia na różne sposoby, zmniejsza liczbę błędów.

Na przykład Haskell ma Maybemonadę zamiast zer. Załóżmy, że DatabaseRecordjest to typ zdefiniowany przez użytkownika. W Haskell wartość typu Maybe DatabaseRecordmoże być równa Just <somevalue>lub może być równa Nothing. Możesz następnie używać go na różne sposoby, ale bez względu na to, jak go używasz, nie możesz zastosować niektórych operacji Nothingbez wiedzy o tym.

Na przykład ta funkcja o nazwie zeroAsDefaultzwraca xdla Just xi 0dla Nothing:

zeroAsDefault :: Maybe Int -> Int
zeroAsDefault mx = case mx of
    Nothing -> 0
    Just x -> x

Christian Hackl mówi, że C ++ 17 i Scala mają swoje własne sposoby. Możesz więc spróbować dowiedzieć się, czy twój język ma coś takiego i użyć go.

Wartości zerowe są nadal w powszechnym użyciu

Jeśli nie masz nic lepszego, używanie nulljest w porządku. Tylko uważaj na to. W każdym razie pomocne będą deklaracje typu w funkcjach.

Może to również brzmieć niezbyt progresywnie, ale powinieneś sprawdzić, czy twoi koledzy chcą z niego skorzystać, nullczy czegoś innego. Mogą być konserwatywne i z pewnych powodów mogą nie chcieć korzystać z nowych struktur danych. Na przykład obsługa starszych wersji języka. Takie rzeczy powinny być zadeklarowane w standardach kodowania projektu i odpowiednio omówione z zespołem.

Na twoją propozycję

Sugerujesz użycie osobnego pola boolowskiego. Ale i tak musisz to sprawdzić i nadal możesz zapomnieć o sprawdzeniu. Więc nic tu nie wygrało. Jeśli możesz nawet zapomnieć o czymś innym, takim jak aktualizacja obu wartości za każdym razem, jest jeszcze gorzej. Jeśli problem zapomnienia o sprawdzeniu nullnie zostanie rozwiązany, nie ma sensu. Unikanie nulljest trudne i nie należy robić tego w sposób, który pogorszy sytuację.

Jak nie używać null

Wreszcie istnieją powszechne sposoby nullnieprawidłowego użycia . Jednym z takich sposobów jest użycie go zamiast pustych struktur danych, takich jak tablice i łańcuchy. Pusta tablica jest właściwą tablicą, jak każda inna! Prawie zawsze jest ważne i przydatne dla struktur danych, które mogą pasować do wielu wartości, aby mogły być puste, tzn. Mają zerową długość.

Z punktu widzenia algebry pusty ciąg znaków dla ciągów jest podobny do 0 dla liczb, tj. Tożsamość:

a+0=a
concat(str, '')=str

Pusty ciąg znaków pozwala ogólnie stać się monoidem: https://en.wikipedia.org/wiki/Monoid Jeśli go nie dostaniesz, nie jest to dla ciebie ważne.

Zobaczmy teraz, dlaczego jest to ważne dla programowania w tym przykładzie:

for (element in array) {
  doSomething(element);
}

Jeśli podamy tutaj pustą tablicę, kod będzie działał poprawnie. Po prostu nic nie zrobi. Jeśli jednak przejdziemy nulltutaj, prawdopodobnie nastąpi awaria z błędem typu „przepraszam, nie można przepuścić przez zero”. Możemy to owinąć, ifale to jest mniej czyste i znowu, możesz zapomnieć to sprawdzić

Jak obsłużyć zerowy

To, co doSomethingAboutIt()powinno być zrobione, a zwłaszcza czy powinno to spowodować wyjątek, to kolejna skomplikowana kwestia. Krótko mówiąc, zależy to od tego, czy nullbyła to dopuszczalna wartość wejściowa dla danego zadania i czego oczekuje się w odpowiedzi. Wyjątek stanowią zdarzenia, których nie oczekiwano. Nie zajmę się tym tematem. Ta odpowiedź jest już bardzo długa.

Gherman
źródło
5
W rzeczywistości horrible error: wanted value but got null!jest znacznie lepszy niż bardziej typowy Segmentation fault. Core dumped....
Toby Speight
2
@TobySpeight Prawda, ale wolałbym coś, co zawiera warunki użytkownika Error: the product you want to buy is out of stock.
Gherman
Jasne - właśnie obserwowałem, że może być (i często jest) jeszcze gorzej . Całkowicie zgadzam się, co byłoby lepsze! A twoja odpowiedź jest prawie tym, co powiedziałbym na to pytanie: +1.
Toby Speight
2
@Gherman: Podanie nullgdzie nulljest niedozwolone jest błędem programowania, tj. Błędem w kodzie. Produkt niedostępny w magazynie jest częścią zwykłej logiki biznesowej i nie jest błędem. Nie można i nie należy próbować tłumaczyć wykrycia błędu na normalną wiadomość w interfejsie użytkownika. Natychmiastowe zawieszanie się i niewykonywanie większej ilości kodu jest preferowanym sposobem działania, szczególnie jeśli jest to ważna aplikacja (np. Wykonująca prawdziwe transakcje finansowe).
Christian Hackl
1
@EricDuminil Chcemy usunąć (lub zmniejszyć) możliwość popełnienia błędu, a nie nullcałkowicie usunąć się nawet z tła.
Gherman
4

Oprócz wszystkich bardzo dobrych wcześniej udzielonych odpowiedzi dodam, że za każdym razem, gdy kusi cię, aby pole było puste, zastanów się, czy może to być typ listy. Typ zerowalny jest równoważnie listą 0 lub 1 elementów i często można go uogólnić na listę N elementów. Szczególnie w tym przypadku warto rozważyć lastChangePasswordTimeutworzenie listy passwordChangeTimes.

Joel
źródło
To doskonały punkt. To samo często dotyczy typów opcji; opcja może być postrzegana jako szczególny przypadek sekwencji.
Christian Hackl
2

Zadaj sobie następujące pytanie: które zachowanie wymaga pola lastChangePasswordTime?

Jeśli potrzebujesz tego pola dla metody IsPasswordExpired (), aby ustalić, czy Członek powinien być monitowany o zmianę hasła co jakiś czas, ustawiłbym w polu czas, w którym Członek był początkowo tworzony. Implementacja IsPasswordExpired () jest taka sama dla nowych i istniejących członków.

class Member{
   private DateTime lastChangePasswordTime;

   public Member(DateTime lastChangePasswordTime) {
      // set value, maybe check for null
   }

   public bool IsPasswordExpired() {
      DateTime limit = DateTime.Now.AddMonths(-3);
      return lastChangePasswordTime < limit;
   }
}

Jeśli masz osobny wymóg, że nowo utworzeni członkowie muszą zaktualizować swoje hasło, dodam osobne pole boolowskie o nazwie passwordShouldBeChanged i ustawię wartość true podczas tworzenia. Chciałbym wtedy zmienić funkcjonalność metody IsPasswordExpired (), aby uwzględnić sprawdzanie tego pola (i zmienić nazwę metody na ShouldChangePassword).

class Member{
   private DateTime lastChangePasswordTime;
   private bool passwordShouldBeChanged;

   public Member(DateTime lastChangePasswordTime, bool passwordShouldBeChanged) {
      // set values, maybe check for nulls
   }

   public bool ShouldChangePassword() {
      return PasswordExpired(lastChangePasswordTime) || passwordShouldBeChanged;
   }

   private static bool PasswordExpired(DateTime lastChangePasswordTime) {
      DateTime limit = DateTime.Now.AddMonths(-3);
      return lastChangePasswordTime < limit;
   }
}

Wyraź swoje intencje w kodzie.

Rik D.
źródło
2

Po pierwsze, zero jako zło jest dogmatem i jak zwykle z dogmatem działa najlepiej jako wytyczna, a nie jako test pozytywny / negatywny.

Po drugie, możesz przedefiniować swoją sytuację w taki sposób, że ma sens, aby wartość nigdy nie była zerowa. InititialPasswordChanged to wartość logiczna początkowo ustawiona na wartość false, PasswordSetTime to data i godzina ustawienia bieżącego hasła.

Należy pamiętać, że chociaż wiąże się to z niewielkim kosztem, możesz ZAWSZE obliczyć, ile czasu minęło od ostatniego ustawienia hasła.

jmoreno
źródło
1

Oba są „bezpieczne / zdrowe / prawidłowe”, jeśli dzwoniący sprawdzi przed użyciem. Problem polega na tym, że dzwoniący nie sprawdzi. Co jest lepsze, jakiś smak błędu zerowego lub użycie niepoprawnej wartości?

Nie ma jednej poprawnej odpowiedzi. To zależy od tego, czym się martwisz.

Jeśli awarie są naprawdę złe, ale odpowiedź nie jest krytyczna lub ma zaakceptowaną wartość domyślną, być może lepiej jest użyć wartości logicznej jako flagi. Jeśli użycie niewłaściwej odpowiedzi jest gorszym problemem niż awaria, lepsze jest użycie wartości null.

W większości „typowych” przypadków szybka awaria i zmuszanie dzwoniącego do sprawdzenia jest najszybszym sposobem na rozsądny kod, a zatem uważam, że domyślnym wyborem powinna być null. Nie uwierzyłbym jednak zbytnio w ewangelistów „X jest źródłem wszelkiego zła”; zwykle nie przewidzieli wszystkich przypadków użycia.

ANone
źródło