Un nuovo campo booleano è migliore di un riferimento null quando un valore può essere significativamente assente?


39

Ad esempio, supponiamo che io abbia una classe Member, che ha un lastChangePasswordTime:

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

il cui lastChangePasswordTime può essere assente in modo significativo, poiché alcuni membri potrebbero non modificare mai le proprie password.

Ma secondo Se i null sono cattivi, cosa dovrebbe essere usato quando un valore può essere significativamente assente? e https://softwareengineering.stackexchange.com/a/12836/248528 , non dovrei usare null per rappresentare un valore significativamente assente. Quindi provo ad aggiungere un flag booleano:

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

Ma penso che sia abbastanza obsoleto perché:

  1. Quando isPasswordChanged è falso, lastChangePasswordTime deve essere nullo e il controllo lastChangePasswordTime == null è quasi identico al controllo isPasswordChanged è falso, quindi preferisco controllare lastChangePasswordTime == null direttamente

  2. Quando modifico la logica qui, potrei dimenticare di aggiornare entrambi i campi.

Nota: quando un utente cambia password, registrerei l'ora in questo modo:

this.lastChangePasswordTime=Date.now();

Il campo booleano aggiuntivo è migliore di un riferimento null qui?


51
Questa domanda è piuttosto specifica del linguaggio, perché ciò che costituisce una buona soluzione dipende in gran parte da ciò che offre il tuo linguaggio di programmazione. Ad esempio, in C ++ 17 o Scala, useresti std::optionalo Option. In altre lingue, potresti dover creare tu stesso un meccanismo appropriato, oppure potresti effettivamente ricorrere a nullqualcosa di simile perché è più idiomatico.
Christian Hackl,

16
C'è un motivo per cui non vuoi che lastChangePasswordTime sia impostato sul tempo di creazione della password (la creazione è una mod, dopo tutto)?
Kristian H,

@ChristianHackl hmm, sono d'accordo sul fatto che ci sono diverse soluzioni "perfette", ma non vedo alcuna lingua (principale) anche se l'uso di un booleano separato sarebbe un'idea migliore in generale rispetto al controllo null / nil. Non sono del tutto sicuro di C / C ++ dato che non sono attivo lì da un po 'di tempo, però.
Frank Hopkins,

@FrankHopkins: un esempio potrebbe essere le lingue in cui le variabili possono essere lasciate non inizializzate, ad esempio C o C ++. lastChangePasswordTimepotrebbe essere un puntatore non inizializzato lì, e confrontarlo con qualsiasi cosa sarebbe un comportamento indefinito. Non è un motivo davvero convincente per non inizializzare il puntatore su NULL/ nullptrinvece, specialmente non nel moderno C ++ (dove non si userebbe affatto un puntatore), ma chi lo sa? Un altro esempio potrebbe essere le lingue senza puntatori, o con un cattivo supporto per i puntatori, forse. (Mi viene in mente FORTRAN 77 ...)
Christian Hackl,

Vorrei usare un enum con 3 casi per questo :)
J. Doe,

Risposte:


72

Non vedo perché, se hai un valore significativamente assente, nullnon dovresti usarlo se sei intenzionale e attento a riguardo.

Se il tuo obiettivo è racchiudere il valore nullable per evitare di fare riferimento inavvertitamente, suggerirei di creare il isPasswordChangedvalore come funzione o proprietà che restituisce il risultato di un controllo null, ad esempio:

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

}

Secondo me, facendo così:

  • Offre una migliore leggibilità del codice rispetto a un controllo null, che potrebbe perdere il contesto.
  • Elimina la necessità che tu debba effettivamente preoccuparti di mantenere il isPasswordChangedvalore che menzioni.

Il modo in cui persistete i dati (presumibilmente in un database) sarebbe responsabile di preservare i valori null.


29
+1 per l'apertura. L'uso sfrenato di null è generalmente disapprovato solo nei casi in cui null è un risultato imprevisto, ovvero dove non avrebbe senso (per un consumatore). Se null è significativo, non è un risultato imprevisto.
Flater,

28
Lo metterei un po 'più forte in quanto non hanno mai due variabili che mantengono lo stesso stato. Fallirà. Nascondere un valore nullo, supponendo che il valore nullo abbia senso all'interno dell'istanza, dall'esterno è un'ottima cosa. In questo caso in cui potresti decidere in seguito che 1970-01-01 indica che la password non è mai stata modificata. Quindi la logica del resto del programma non deve preoccuparsi.
Piegato il

3
Potresti dimenticare di chiamare isPasswordChangedproprio come puoi dimenticare di controllare se è nullo. Non vedo niente guadagnato qui.
Gherman,

9
@Gherman Se il consumatore non ottiene il concetto che null è significativo o che deve verificare il valore presente, si perde in entrambi i modi, ma se viene utilizzato il metodo, è chiaro a tutti che leggono il codice perché è un controllo e che esiste una ragionevole possibilità che il valore sia nullo / non presente. Altrimenti non è chiaro se fosse solo uno sviluppatore ad aggiungere un controllo null solo "perché no" o se fa parte del concetto. Certo, uno può scoprirlo, ma in un modo hai le informazioni direttamente l'altro devi esaminare l'implementazione.
Frank Hopkins,

2
@FrankHopkins: buon punto, ma se si utilizza la concorrenza, anche l'ispezione di una singola variabile potrebbe richiedere il blocco.
Christian Hackl,

63

nullsnon sono malvagi. Usarli senza pensare è. Questo è un caso in cui nullè esattamente la risposta corretta: non esiste una data.

Nota che la tua soluzione crea più problemi. Qual è il significato della data impostata su qualcosa, ma isPasswordChangedè falso? Hai appena creato un caso di informazioni contrastanti che devi catturare e trattare in modo speciale, mentre un nullvalore ha un significato chiaramente definito e non ambiguo e non può essere in conflitto con altre informazioni.

Quindi no, la tua soluzione non è migliore. Consentire un nullvalore è l'approccio giusto qui. Le persone che affermano che nullè sempre malvagia, indipendentemente dal contesto, non capiscono perché nullesiste.


17
Storicamente, nullesiste perché Tony Hoare ha commesso l'errore di un miliardo di dollari nel 1965. Le persone che affermano che nullè "malvagio" stanno ovviamente semplificando eccessivamente l'argomento, ma è una buona cosa che i linguaggi moderni o gli stili di programmazione si stanno allontanando null, sostituendo con tipi di opzioni dedicati.
Christian Hackl,

9
@ChristianHackl: appena per interesse storico - die Hoare ha mai detto quale sarebbe stata la migliore alternativa pratica (senza passare a un paradigma completamente nuovo)?
AnoE,

5
@AnoE: domanda interessante. Non so cosa dire Hoare su questo, ma la programmazione senza null è spesso una realtà in questi giorni in linguaggi di programmazione moderni e ben progettati.
Christian Hackl,

10
@Tom wat? In lingue come C # e Java, il DateTimecampo è un puntatore. L'intero concetto nullha senso solo per i puntatori!
Todd Sewell,

16
@Tom: i puntatori null sono pericolosi solo per i linguaggi e le implementazioni che consentono di indicizzarli alla cieca e di non farvi sapere, con qualunque esilarazione possa derivare. In un linguaggio che intercetta i tentativi di indicizzare o dereferenziare un puntatore nullo, sarà spesso meno pericoloso di un puntatore a un oggetto fittizio. Ci sono molte situazioni in cui avere un programma che termina in modo anomalo può essere preferibile al fatto che produca numeri insignificanti, e sui sistemi che li intercettano, i puntatori null sono la ricetta giusta per questo.
Supercat,

29

A seconda del linguaggio di programmazione, potrebbero esserci buone alternative, come un optionaltipo di dati. In C ++ (17), questo sarebbe std :: opzionale . Può essere assente o avere un valore arbitrario del tipo di dati sottostante.


8
C'è Optionalanche java; il significato di un nulla Optionalche dipende da te ...
Matthieu M.

1
Sono venuto qui per connettermi anche con Opzionale. Lo codificherei come un tipo in una lingua che li aveva.
Zipp,

2
@FrankHopkins È un peccato che nulla in Java ti impedisca di scrivere Optional<Date> lastPasswordChangeTime = null;, ma non mi arrenderei proprio per questo. Invece, instillerei una regola di squadra molto ferma, che non è mai possibile assegnare valori opzionali null. Opzionale ti compra troppe funzioni carine per rinunciarvi facilmente. Puoi facilmente assegnare valori predefiniti ( orElseo con valutazione pigra:) orElseGet, lanciare errori ( orElseThrow), trasformare i valori in un modo nullo sicuro ( map, flatmap), ecc.
Alexander - Ripristina Monica il

5
@FrankHopkins Il controllo isPresentè quasi sempre un odore di codice, nella mia esperienza, e un segno abbastanza affidabile che gli sviluppatori che usano questi opzionali "mancano il punto". In effetti, non offre praticamente alcun vantaggio ref == null, ma non è qualcosa che dovresti usare spesso. Anche se il team è instabile, uno strumento di analisi statica può stabilire la legge, come una linter o FindBugs , che può catturare la maggior parte dei casi.
Alexander - Ripristina Monica il

5
@FrankHopkins: è possibile che la comunità Java abbia solo bisogno di un piccolo cambio di paradigma, che potrebbe richiedere ancora molti anni. Se si guarda alla Scala, per esempio, sostiene null, perché la lingua è al 100% compatibile con Java, ma nessuno lo usa, ed Option[T]è tutto il luogo, con un eccellente supporto funzionale-di programmazione come map, flatMap, pattern matching e così via, che va molto, molto al di là dei == nullcontrolli. Hai ragione sul fatto che in teoria un tipo di opzione sembra poco più che un puntatore glorificato, ma la realtà dimostra che l'argomento è sbagliato.
Christian Hackl,

11

Utilizzo corretto di null

Esistono diversi modi di utilizzo null. Il modo più comune e semanticamente corretto è usarlo quando potresti avere o meno un singolo valore. In questo caso un valore è uguale nullo è qualcosa di significativo come un record dal database o qualcosa del genere.

In queste situazioni, lo usi principalmente in questo modo (in pseudo-codice):

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

doSomethingUseful(value);

Problema

E ha un grosso problema. Il problema è che quando invochi doSomethingUsefulil valore potrebbe non essere stato verificato null! In caso contrario, probabilmente il programma si arresterà in modo anomalo. E l'utente potrebbe anche non vedere alcun buon messaggio di errore, rimanendo con qualcosa come "errore orribile: valore desiderato ma ottenuto nullo!" (dopo l'aggiornamento: anche se Segmentation fault. Core dumped.in alcuni casi potrebbe esserci un errore informativo ancora minore come , o peggio ancora, nessun errore e manipolazione errata su null)

Dimenticare di scrivere controlli nulle gestire nullsituazioni è un bug estremamente comune . Ecco perché Tony Hoare, che ha inventato, ha nulldichiarato in una conferenza sul software chiamata QCon London nel 2009 di aver commesso l'errore di un miliardo di dollari nel 1965: https://www.infoq.com/presentations/Null-References-The-Billion-Dollar- errore-Tony-Hoare

Evitare il problema

Alcune tecnologie e lingue rendono nullimpossibile il controllo dell'oblio in diversi modi, riducendo la quantità di bug.

Ad esempio Haskell ha la Maybemonade invece di null. Supponiamo che DatabaseRecordsia un tipo definito dall'utente. In Haskell un valore di tipo Maybe DatabaseRecordpuò essere uguale Just <somevalue>o uguale a Nothing. È quindi possibile utilizzarlo in diversi modi, ma indipendentemente da come lo si utilizza non è possibile applicare alcune operazioni Nothingsenza saperlo.

Ad esempio questa funzione chiamata zeroAsDefaultrestituisce xper Just xe 0per Nothing:

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

Christian Hackl afferma che C ++ 17 e Scala hanno i loro modi. Quindi potresti voler provare a scoprire se la tua lingua ha qualcosa del genere e usarla.

I null sono ancora ampiamente utilizzati

Se non hai niente di meglio, allora usare nullva bene. Continua a fare attenzione. Digitare le dichiarazioni nelle funzioni ti aiuterà comunque in qualche modo.

Inoltre, potrebbe non sembrare molto progressivo, ma dovresti controllare se i tuoi colleghi vogliono usare nullo qualcos'altro. Potrebbero essere prudenti e potrebbero non voler utilizzare nuove strutture dati per alcuni motivi. Ad esempio, supportando versioni precedenti di una lingua. Tali cose dovrebbero essere dichiarate negli standard di codifica del progetto e adeguatamente discusse con il team.

Sulla tua proposta

Suggerisci di usare un campo booleano separato. Ma devi comunque controllarlo e potresti ancora dimenticartene. Quindi non c'è niente vinto qui. Se puoi anche dimenticare qualcos'altro, come aggiornare entrambi i valori ogni volta, allora è anche peggio. Se il problema di dimenticare di verificare nullnon è risolto, non ha senso. Evitare nullè difficile e non dovresti farlo in modo da peggiorare le cose.

Come non usare null

Infine, ci sono modi comuni per utilizzare in nullmodo errato. Uno di questi è quello di usarlo al posto di strutture di dati vuote come array e stringhe. Un array vuoto è un array adeguato come qualsiasi altro! È quasi sempre importante e utile per le strutture di dati, che possono adattarsi a più valori, per poter essere vuoti, ovvero avere lunghezza 0.

Dal punto di vista dell'algebra una stringa vuota per le stringhe è molto simile a 0 per i numeri, ovvero identità:

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

La stringa vuota consente alle stringhe in generale di diventare un monoide: https://en.wikipedia.org/wiki/Monoid Se non lo ottieni non è così importante per te.

Ora vediamo perché è importante per la programmazione con questo esempio:

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

Se passiamo un array vuoto qui il codice funzionerà bene. Non farà proprio niente. Tuttavia, se passiamo un nullqui, probabilmente avremo un arresto anomalo con un errore del tipo "impossibile passare da null a, scusa". Potremmo avvolgerlo ifma è meno pulito e di nuovo, potresti dimenticare di controllarlo

Come gestire null

Ciò che doSomethingAboutIt()dovrebbe essere fatto e soprattutto se dovrebbe generare un'eccezione è un altro problema complicato. In breve, dipende dal fatto che nullfosse un valore di input accettabile per una determinata attività e da cosa ci si aspetta in risposta. Le eccezioni sono per eventi che non erano previsti. Non approfondirò questo argomento. Questa risposta è già molto lunga.


5
In realtà, horrible error: wanted value but got null!è molto meglio del più tipico Segmentation fault. Core dumped....
Toby Speight,

2
@TobySpeight Vero, ma preferirei qualcosa che risieda nei termini dell'utente come Error: the product you want to buy is out of stock.
Gherman,

Certo, stavo solo osservando che potrebbe essere (e spesso lo è) anche peggio . Sono completamente d'accordo su cosa sarebbe meglio! E la tua risposta è praticamente ciò che avrei detto a questa domanda: +1.
Toby Speight,

2
@Gherman: passare nulldove nullnon è consentito è un errore di programmazione, cioè un bug nel codice. Un prodotto esaurito fa parte della normale logica aziendale e non è un bug. Non è possibile e non si dovrebbe tentare di tradurre il rilevamento di un bug in un normale messaggio nell'interfaccia utente. L'arresto immediato e la mancata esecuzione di più codice è la linea d'azione preferita, soprattutto se si tratta di un'applicazione importante (ad esempio, che esegue transazioni finanziarie reali).
Christian Hackl,

1
@EricDuminil Vogliamo rimuovere (o ridurre) la possibilità di fare un errore, non rimuoverlo del nulltutto, anche dallo sfondo.
Gherman,

4

Oltre a tutte le ottime risposte fornite in precedenza, aggiungerei che ogni volta che sei tentato di lasciare un campo nullo, pensa attentamente se potrebbe essere un tipo di elenco. Un tipo nullable è equivalentemente un elenco di 0 o 1 elementi, e spesso questo può essere generalizzato a un elenco di N elementi. Nello specifico in questo caso, potresti considerare di lastChangePasswordTimeessere un elenco di passwordChangeTimes.


Questo è un punto eccellente. Lo stesso vale spesso per i tipi di opzione; un'opzione può essere vista come caso speciale di una sequenza.
Christian Hackl,

2

Chiediti questo: quale comportamento richiede il campo lastChangePasswordTime?

Se è necessario quel campo per un metodo IsPasswordExpired () per determinare se un Membro dovrebbe essere invitato a cambiare la propria password ogni tanto, impostarei il campo al momento della creazione iniziale del Membro. L'implementazione IsPasswordExpired () è la stessa per i membri nuovi ed esistenti.

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;
   }
}

Se hai un requisito separato che i membri di nuova creazione devono aggiornare la loro password, aggiungerei un campo booleano separato chiamato passwordShouldBeChanged e lo imposto su true al momento della creazione. Vorrei quindi modificare la funzionalità del metodo IsPasswordExpired () per includere un controllo per quel campo (e rinominare il metodo in 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;
   }
}

Rendi esplicite le tue intenzioni nel codice.


2

In primo luogo, essendo nulli il male è dogma e come al solito con dogma funziona meglio come linea guida e non come test passa / non passa.

In secondo luogo, puoi ridefinire la tua situazione in modo che abbia senso che il valore non possa mai essere nullo. InititialPasswordChanged è un valore booleano inizialmente impostato su false, PasswordSetTime è la data e l'ora in cui è stata impostata la password corrente.

Si noti che sebbene ciò abbia un leggero costo, ora è possibile calcolare SEMPRE quanto tempo è trascorso dall'ultima impostazione della password.


1

Entrambi sono "sicuri / sani / corretti" se il chiamante controlla prima dell'uso. Il problema è cosa succede se il chiamante non controlla. Quale è meglio, qualche tipo di errore nullo o l'utilizzo di un valore non valido?

Non esiste un'unica risposta corretta. Dipende da cosa ti preoccupi.

Se gli arresti anomali sono davvero gravi ma la risposta non è critica o ha un valore predefinito accettato, forse usare un booleano come flag è meglio. Se usare la risposta sbagliata è un problema peggiore del crash, allora usare null è meglio.

Per la maggior parte dei casi "tipici", il fallimento rapido e la forzatura dei chiamanti a controllare è il modo più veloce per far funzionare il codice, e quindi penso che null sia la scelta predefinita. Non darei troppa fiducia agli evangelisti "X è la radice di tutti i mali"; normalmente non hanno anticipato tutti i casi d'uso.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.