I riferimenti null sono davvero una brutta cosa?


161

Ho sentito dire che l'inclusione di riferimenti null nei linguaggi di programmazione è "l'errore di miliardi di dollari". Ma perché? Certo, possono causare NullReferenceExceptions, ma che importa? Qualsiasi elemento del linguaggio può essere fonte di errori se utilizzato in modo improprio.

E qual è l'alternativa? Suppongo invece di dire questo:

Customer c = Customer.GetByLastName("Goodman"); // returns null if not found
if (c != null)
{
    Console.WriteLine(c.FirstName + " " + c.LastName + " is awesome!");
}
else { Console.WriteLine("There was no customer named Goodman.  How lame!"); }

Potresti dire questo:

if (Customer.ExistsWithLastName("Goodman"))
{
    Customer c = Customer.GetByLastName("Goodman") // throws error if not found
    Console.WriteLine(c.FirstName + " " + c.LastName + " is awesome!"); 
}
else { Console.WriteLine("There was no customer named Goodman.  How lame!"); }

Ma come va meglio? Ad ogni modo, se si dimentica di verificare l'esistenza del cliente, si ottiene un'eccezione.

Suppongo che una CustomerNotFoundException sia un po 'più facile da eseguire il debug di una NullReferenceException in quanto più descrittiva. È tutto quello che c'è da fare?


54
Diffiderei dell'approccio "se il cliente esiste / quindi ottieni il cliente", non appena scrivi due righe di codice del genere, apri la porta alle condizioni di gara. Ottieni e quindi verifica la presenza di eccezioni null / catch è più sicuro.
Carson63000,

26
Se solo potessimo eliminare la fonte di tutti gli errori di runtime, il nostro codice sarebbe garantito per essere perfetto! Se i riferimenti NULL in C # ti rompono il cervello, prova i puntatori non validi, ma non NULL, in C;)
LnxPrgr3

ci sono gli operatori nulli di coalescenza e navigazione sicura in alcune lingue
jk.

35
null di per sé non è male. Un sistema di tipi che non fa alcuna differenza tra il tipo T e il tipo T + null è errato.
Ingo,

27
Tornando alla mia domanda qualche anno dopo, ora sono totalmente un convertito dell'approccio Opzione / Forse.
Tim Goodman,

Risposte:


91

null è male

C'è una presentazione su InfoQ su questo argomento: Riferimenti nulli: l'errore di miliardi di dollari di Tony Hoare

Tipo di opzione

L'alternativa alla programmazione funzionale sta usando un tipo di opzione , che può contenere SOME valueo NONE.

Un buon articolo Il modello "Opzione" che discute il tipo di opzione e ne fornisce un'implementazione per Java.

Ho anche trovato un bug report per Java su questo problema: aggiungi tipi di Nice Option a Java per prevenire NullPointerExceptions . La funzione richiesta è stata introdotta in Java 8.


5
Nullable <x> in C # è un concetto simile ma non è certo un'implementazione del modello di opzioni. Il motivo principale è che in .NET System.Nullable è limitato ai tipi di valore.
MattDavey,

27
+1 per il tipo di opzione. Dopo aver familiarizzato con Forse di Haskell , i null iniziano a sembrare strani ...
Andres F.

5
Lo sviluppatore può commettere errori ovunque, quindi invece dell'eccezione puntatore null otterrai un'eccezione di opzione vuota. In che modo il secondo è migliore del primo?
Greenoldman,

7
@greenoldman non esiste una cosa come "eccezione opzione vuota". Prova a inserire valori NULL nelle colonne del database definite come NOT NULL.
Jonas,

36
@greenoldman Il problema con i null è che qualsiasi cosa può essere nulla e come sviluppatore devi essere molto cauto. Un Stringtipo non è in realtà una stringa. È un String or Nulltipo. Le lingue che aderiscono pienamente all'idea dei tipi di opzione non consentono valori null nel loro sistema di tipi, garantendo che se si definisce una firma di tipo che richiede un String(o qualsiasi altra cosa), deve avere un valore. Il Optiontipo è lì per fornire una rappresentazione a livello di tipo di cose che possono o non possono avere un valore, quindi sai dove devi gestire esplicitamente quelle situazioni.
KChaloux,

127

Il problema è che, poiché in teoria qualsiasi oggetto può essere nullo e generare un'eccezione quando si tenta di utilizzarlo, il codice orientato agli oggetti è fondamentalmente una raccolta di bombe inesplose.

Hai ragione sul fatto che una buona gestione degli errori può essere funzionalmente identica alle ifdichiarazioni di null null . Ma cosa succede quando qualcosa che ti sei convinto di non poter essere un null è, in realtà, un null? Kerboom. Qualunque cosa accada dopo, sono disposto a scommettere che 1) non sarà grazioso e 2) non ti piacerà.

E non ignorare il valore di "facile debug". Il codice di produzione maturo è una creatura pazza e tentacolare; tutto ciò che ti dà più informazioni su cosa è andato storto e dove potrebbe farti risparmiare ore di scavo.


27
+1 Di solito gli errori nel codice di produzione DEVONO essere identificati il ​​più rapidamente possibile in modo da poter essere corretti (di solito errore di dati). Più è facile eseguire il debug, meglio è.

4
Questa risposta è la stessa se applicata a uno degli esempi del PO. O verifichi le condizioni di errore oppure no, e le gestisci con garbo oppure no. In altre parole, la rimozione di riferimenti null dalla lingua non modifica le classi di errori che si verificheranno, modificherà semplicemente la manifestazione di tali errori.
dash-tom-bang,

19
+1 per bombe inesplose. Con riferimenti null, dire public Foo doStuff()non significa "fare cose e restituire il risultato Foo" significa "fare cose e restituire il risultato Foo, O restituire null, ma non si ha idea se ciò accadrà o meno". Il risultato è che devi controllare NULLA OVUNQUE per essere certi. Potrebbe anche rimuovere i null e utilizzare un tipo o un modello speciale (come un tipo di valore) per indicare un valore insignificante.
Matt Olenik,

12
@greenoldman, il tuo esempio è doppiamente efficace per dimostrare il nostro punto in cui l'uso di tipi primitivi (siano essi nulli o interi) come modelli semantici è una cattiva pratica. Se si dispone di un tipo per il quale i numeri negativi non sono una risposta valida, è necessario creare una nuova classe di tipi per implementare il modello semantico con quel significato. Ora tutto il codice per gestire la modellazione di quel tipo non negativo è localizzato nella sua classe, isolato dal resto del sistema, e qualsiasi tentativo di creare un valore negativo per esso può essere colto immediatamente.
Huperniketes

9
@greenoldman, continui a dimostrare che i tipi primitivi sono inadeguati per la modellazione. Prima era su numeri negativi. Ora è sopra l'operatore di divisione quando zero è il divisore. Se sai che non puoi dividere per zero, puoi prevedere che il risultato non sarà abbastanza, e sei responsabile di assicurarti di testare e fornire il valore "valido" appropriato per quel caso. Gli sviluppatori di software sono responsabili della conoscenza dei parametri in cui opera il loro codice e della codifica appropriata. È ciò che distingue l'ingegneria dall'armeggiare.
Huperniketes,

50

Esistono diversi problemi con l'utilizzo di riferimenti null nel codice.

Innanzitutto, viene generalmente utilizzato per indicare uno stato speciale . Piuttosto che definire una nuova classe o costante per ogni stato quando vengono fatte le specializzazioni, usando un riferimento null si usa un tipo / valore con perdita generalizzata in modo massiccio .

In secondo luogo, il codice di debug diventa più difficile quando viene visualizzato un riferimento null e si tenta di determinare cosa lo ha generato , quale stato è in vigore e la sua causa anche se è possibile tracciare il percorso di esecuzione a monte.

In terzo luogo, i riferimenti null introducono percorsi di codice aggiuntivi da testare .

In quarto luogo, una volta che i riferimenti null sono utilizzati come stati validi per i parametri, nonché i valori di ritorno, la programmazione difensiva (per gli stati causati dalla progettazione) richiede un controllo più nullo dei riferimenti da eseguire in vari punti ... per ogni evenienza.

In quinto luogo, il runtime della lingua sta già eseguendo controlli del tipo quando esegue la ricerca del selettore sulla tabella dei metodi di un oggetto. Quindi stai duplicando lo sforzo controllando se il tipo di oggetto è valido / non valido e quindi il runtime controlla il tipo di oggetto valido per invocare il suo metodo.

Perché non utilizzare il modello NullObject per trarre vantaggio dal controllo del runtime per fare in modo che invochi metodi NOP specifici per quello stato (conforme all'interfaccia dello stato normale) eliminando al contempo tutto il controllo aggiuntivo per riferimenti null in tutta la base di codice?

Si tratta di più di lavoro creando una classe NullObject per ogni interfaccia con la quale si vuole rappresentare uno stato speciale. Ma almeno la specializzazione è isolata per ogni stato speciale, piuttosto che per il codice in cui lo stato potrebbe essere presente. IOW, il numero di test è ridotto perché hai meno percorsi di esecuzione alternativi nei tuoi metodi.


7
... perché capire chi ha generato (o meglio, non si è preso la briga di cambiare o inizializzare) i dati spazzatura che sta riempiendo il tuo oggetto apparentemente utile è molto più facile che rintracciare un riferimento NULL? Non lo compro, anche se per lo più sono bloccato in terra tipicamente statica.
dash-tom-bang,

Tuttavia, i dati di immondizia sono molto più rari dei riferimenti null. Soprattutto nelle lingue gestite.
Dean Harding,

5
-1 per Null Object.
DeadMG

4
I punti (4) e (5) sono solidi, ma NullObject non è un rimedio. Cadrai in errori trappola logici (flusso di lavoro) ancora peggiori. Quando conosci ESATTAMENTE la situazione attuale, sicuramente può essere d'aiuto, ma usarla alla cieca (anziché a null) ti costerà di più.
Greenoldman,

1
@ gnasher729, mentre il runtime di Objective-C non genererà un'eccezione puntatore null come fanno Java e altri sistemi, non rende migliore l'utilizzo di riferimenti null per i motivi che ho delineato nella mia risposta. Ad esempio, se non c'è navigationController nel [self.navigationController.presentingViewController dismissViewControllerAnimated: YES completion: nil];runtime lo tratta come una sequenza di no-op, la vista non viene rimossa dal display e potresti sederti davanti a Xcode a grattarti la testa per ore cercando di capire perché.
Huperniketes

48

I nulli non sono poi così male, a meno che non te li aspetti. Si dovrebbe necessario specificare in modo esplicito nel codice che vi aspettate nulla, che è un problema di lingua-design. Considera questo:

Customer? c = Customer.GetByLastName("Goodman");
// note the question mark -- 'c' is either a Customer or null
if (c != null)
{
    // c is not null, therefore its type in this block is
    // now 'Customer' instead of 'Customer?'
    Console.WriteLine(c.FirstName + " " + c.LastName + " is awesome!");
}
else { Console.WriteLine("There was no customer named Goodman.  How lame!"); }

Se si tenta di invocare metodi su un Customer?, si dovrebbe ottenere un errore di compilazione. Il motivo per cui più lingue non lo fanno (IMO) è che non prevedono che il tipo di una variabile cambi a seconda dell'ambito in cui si trova. Se la lingua è in grado di gestirlo, il problema può essere risolto interamente all'interno del tipo di sistema.

C'è anche il modo funzionale di gestire questo problema, usando i tipi di opzioni e Maybe, ma non ne ho familiarità. Preferisco così perché il codice corretto dovrebbe teoricamente richiedere solo un carattere aggiunto per compilare correttamente.


11
Wow, è abbastanza bello. Oltre a rilevare molti più errori in fase di compilazione, mi risparmia di dover controllare la documentazione per capire se GetByLastNamerestituisce null se non trovato (al contrario di lanciare un'eccezione) - Posso solo vedere se ritorna Customer?o Customer.
Tim Goodman,

14
@JBRWilkinson: questo è solo un esempio inventato per mostrare l'idea, non un esempio di un'implementazione effettiva. In realtà penso che sia un'idea abbastanza chiara.
Dean Harding,

1
Hai ragione però che non è il modello di oggetti null.
Dean Harding,

13
Questo sembra quello che Haskell chiama a Maybe- A Maybe Customerè Just c(dove cè a Customer) o Nothing. In altre lingue, si chiama Option: vedi alcune delle altre risposte a questa domanda.
MatrixFrog,

2
@SimonBarker: puoi essere sicuro che Customer.FirstNamesia di tipo String(al contrario di String?). Questo è il vero vantaggio - solo le variabili / proprietà che hanno un significativo niente valore deve essere dichiarato come annullabile.
Yogu,

20

Ci sono molte risposte eccellenti che coprono gli sfortunati sintomi di null, quindi vorrei presentare un argomento alternativo: Null è un difetto nel sistema dei tipi.

Lo scopo di un sistema di tipi è quello di garantire che i diversi componenti di un programma "si incastrino" correttamente; un programma ben tipizzato non può "spaccare le rotaie" in un comportamento indefinito.

Prendi in considerazione un ipotetico dialetto di Java, o qualunque sia la tua lingua preferita digitata staticamente, in cui puoi assegnare la stringa "Hello, world!"a qualsiasi variabile di qualsiasi tipo:

Foo foo1 = new Foo();  // Legal
Foo foo2 = "Hello, world!"; // Also legal
Foo foo3 = "Bonjour!"; // Not legal - only "Hello, world!" is allowed

E puoi controllare le variabili in questo modo:

if (foo1 != "Hello, world!") {
    bar(foo1);
} else {
    baz();
}

Non c'è nulla di impossibile in questo - qualcuno potrebbe progettare un linguaggio del genere se lo volesse. Il valore speciale non deve essere "Hello, world!"- potrebbe essere stato il numero 42, la tupla (1, 4, 9), o, diciamo, null. Ma perché dovresti farlo? Una variabile di tipo Foodovrebbe contenere solo Foos - questo è l'intero punto del sistema di tipi! nullnon è Foopiù ciò che "Hello, world!"è. Peggio ancora, nullnon è un valore di alcun tipo e non c'è niente che tu possa farci!

Il programmatore non può mai essere sicuro che una variabile contenga effettivamente un Foo, e nemmeno il programma; per evitare comportamenti indefiniti, deve controllare le variabili "Hello, world!"prima di usarle come Foos. Nota che eseguire il controllo delle stringhe nello snippet precedente non propaga il fatto che foo1 sia davvero un Foo- barprobabilmente avrà anche un proprio controllo, solo per sicurezza.

Confronta quello con l'uso di un tipo Maybe/ Optioncon la corrispondenza del modello:

case maybeFoo of
 |  Just foo => bar(foo)
 |  Nothing => baz()

All'interno della Just fooclausola, sia tu che il programma sapete con certezza che la nostra Maybe Foovariabile contiene davvero un Foovalore - che le informazioni vengono propagate lungo la catena di chiamate e barnon è necessario effettuare alcun controllo. Perché Maybe Fooè un tipo distinto da Foo, sei costretto a gestire la possibilità che possa contenere Nothing, quindi non puoi mai essere accecato da un NullPointerException. Puoi ragionare sul tuo programma molto più facilmente e il compilatore può omettere controlli null sapendo che tutte le variabili di tipo Foocontengono davvero Foos. Tutti vincono.


Che dire dei valori null-like in Perl 6? Sono ancora nulli, tranne per il fatto che sono sempre digitati. È ancora un problema che puoi scrivere my Int $variable = Int, dov'è il Intvalore null di tipo Int?
Konrad Borowski,

@xfix Non ho familiarità con Perl, ma fintanto che non hai un modo di far valere this type only contains integersil contrario this type contains integers and this other value, avrai un problema ogni volta che vuoi solo numeri interi.
Doval,

1
+1 per la corrispondenza dei motivi. Con il numero di risposte sopra menzionato i tipi di Opzione, penseresti che qualcuno avrebbe menzionato il fatto che una semplice funzionalità linguistica come i pattern matcher può renderli molto più intuitivi con cui lavorare rispetto ai null.
Jules,

1
Penso che il legame monadico sia ancora più utile del pattern matching (anche se ovviamente il binding per Maybe è implementato usando il pattern matching). Penso che sia una specie di divertenti lingue vedendo come C # mettendo sintassi speciale dentro per un sacco di cose ( ??, ?., e così via) per le cose che i linguaggi come Haskell implementano funzioni di libreria. Penso che sia molto più pulito. null/ Nothingpropagation non è "speciale" in alcun modo, quindi perché dovremmo imparare più sintassi per averlo?
Sara

14

(Lanciando il mio cappello sul ring per una vecchia domanda;))

Il problema specifico con null è che interrompe la tipizzazione statica.

Se ho un Thing t, quindi il compilatore può garantire che posso chiamare t.doSomething(). Bene, UNLESS tè nullo in fase di esecuzione. Ora tutte le scommesse sono spente. Il compilatore ha detto che era OK, ma scopro molto più tardi che tNON lo è doSomething(). Quindi, anziché essere in grado di fidarmi del compilatore per rilevare errori di tipo, devo attendere fino al momento dell'esecuzione per rilevarli. Potrei anche usare Python!

Quindi, in un certo senso, null introduce la tipizzazione dinamica in un sistema tipizzato staticamente, con risultati attesi.

La differenza tra una divisione per zero o log di negativo, ecc. È che quando i = 0, è ancora un int. Il compilatore può ancora garantire il suo tipo. Il problema è che la logica applica erroneamente quel valore in un modo che non è consentito dalla logica ... ma se la logica lo fa, è praticamente la definizione di un bug.

(Il compilatore può rilevare alcuni di questi problemi, a proposito. Come espressioni flagganti come i = 1 / 0in fase di compilazione. Ma non puoi davvero aspettarti che il compilatore segua una funzione e assicuri che tutti i parametri siano coerenti con la logica della funzione)

Il problema pratico è che fai molto lavoro extra e aggiungi controlli null per proteggerti in fase di esecuzione, ma cosa succede se ne dimentichi uno? Il compilatore ti impedisce di assegnare:

String s = new Integer (1234);

Quindi perché dovrebbe consentire l'assegnazione a un valore (null) che interromperà i de-riferimenti s?

Mescolando "nessun valore" con riferimenti "digitati" nel codice, stai caricando ulteriormente i programmatori. E quando si verificano NullPointerExceptions, rintracciarle può richiedere ancora più tempo. Invece di fare affidamento sulla digitazione statica per dire "questo è un riferimento a qualcosa di previsto", stai lasciando che il linguaggio dica "questo potrebbe benissimo essere un riferimento a qualcosa di previsto".


3
In C, una variabile di tipo *intidentifica un int, o NULL. Allo stesso modo in C ++, una variabile di tipo *Widgetidentifica a Widget, una cosa di cui deriva il tipo Widgeto NULL. Il problema con Java e derivati ​​come C # è che usano la posizione di memoria Widgetper significare *Widget. La soluzione non è fingere che *Widgetnon dovrebbe essere in grado di contenere NULL, ma piuttosto di avere un Widgettipo di posizione di archiviazione adeguato .
supercat,

14

Un nullpuntatore è uno strumento

non un nemico. Dovresti solo usarli nel modo giusto.

Pensa solo un minuto al tempo impiegato per trovare e correggere un tipico bug puntatore non valido , rispetto alla localizzazione e correzione di un bug puntatore null. È facile controllare un puntatore contro null. È un PITA per verificare se un puntatore non nullo punta o meno a dati validi.

Se non sei ancora convinto, aggiungi una buona dose di multithreading al tuo scenario, quindi ripensaci.

Morale della storia: non gettare i bambini con l'acqua. E non dare la colpa agli strumenti per l'incidente. L'uomo che ha inventato il numero zero molto tempo fa era abbastanza intelligente, da quel giorno non si poteva nominare il "nulla". Il nullpuntatore non è così lontano da quello.


EDIT: Anche se il NullObjectmodello sembra essere una soluzione migliore rispetto ai riferimenti che possono essere nulli, introduce problemi da solo:

  • Un riferimento che tiene un NullObjectdovrebbe (secondo la teoria) non fa nulla quando viene chiamato un metodo. Pertanto, errori sottili possono essere introdotti a causa di riferimenti non assegnati in modo errato, che ora sono garantiti come non nulli (yehaa!) Ma eseguono un'azione indesiderata: nulla. Con un NPE è quasi ovvio dove si trova il problema. Con un NullObjectcomportamento che si comporta in qualche modo (ma è sbagliato), introduciamo il rischio che errori vengano rilevati (troppo) in ritardo. Questo non è per caso simile a un problema di puntatore non valido e ha conseguenze simili: il riferimento punta a qualcosa, che assomiglia a dati validi, ma non lo è, introduce errori di dati e può essere difficile da rintracciare. Ad essere onesti, in questi casi preferirei senza battere ciglio un NPE che fallisce immediatamente, ora,per un errore logico / di dati che improvvisamente mi colpisce in seguito. Ricordi lo studio IBM sul costo degli errori in funzione della fase in cui vengono rilevati?

  • La nozione di non fare nulla quando viene chiamato un metodo su a NullObjectnon regge, quando viene chiamato un getter di proprietà o una funzione o quando il metodo restituisce valori tramite out. Diciamo che il valore restituito è un inte decidiamo che "non fare nulla" significa "ritorno 0". Ma come possiamo essere sicuri, che 0 è il "niente" giusto per essere (non) restituito? Dopotutto, NullObjectnon dovrebbe fare nulla, ma quando viene chiesto un valore, deve reagire in qualche modo. Fallire non è un'opzione: usiamo il NullObjectper prevenire l'NPE e sicuramente non lo scambieremo con un'altra eccezione (non implementata, dividere per zero, ...), vero? Quindi, come restituire correttamente nulla quando si deve restituire qualcosa?

Non posso fare a meno, ma quando qualcuno cerca di applicare il NullObjectmodello a ogni singolo problema, sembra più come cercare di riparare un errore facendo un altro. È senza dubbio una buona e utile soluzione in alcuni casi, ma sicuramente non è il proiettile magico per tutti i casi.


Puoi presentare una discussione sul perché nulldovrebbe esistere? È possibile eseguire l'ondeggiamento manuale di qualsiasi difetto linguistico con "non è un problema, ma non commettere errori".
Doval,

A quale valore imposteresti un puntatore non valido?
JensG,

Bene, non li imposti su alcun valore, perché non dovresti permetterli affatto. Invece, usi una variabile di tipo diverso, forse chiamata Maybe Tche può contenere uno Nothingo un Tvalore. La cosa chiave qui è che sei costretto a verificare se un Maybecontiene davvero un Tprima che ti sia permesso di usarlo, e fornire una linea d'azione nel caso in cui non lo faccia. E poiché hai vietato i puntatori non validi, ora non devi mai controllare nulla che non sia un Maybe.
Doval,

1
@JensG nel tuo ultimo esempio, il compilatore ti fa fare il controllo null prima di ogni chiamata a t.Something()? Oppure ha un modo per sapere che hai già fatto il controllo e non farti ripetere? Che cosa succede se si esegue il controllo prima di passare ta questo metodo? Con il tipo Opzione, il compilatore sa se hai ancora eseguito il controllo osservando il tipo di t. Se è un'opzione, non l'hai verificata, mentre se è il valore non scartato allora hai.
Tim Goodman,

1
Cosa restituisce l'oggetto null quando viene richiesto un valore?
JensG

8

I riferimenti null sono un errore perché consentono il codice non sensitivo:

foo = null
foo.bar()

Esistono alternative se si utilizza il sistema dei tipi:

Maybe<Foo> foo = null
foo.bar() // error{Maybe<Foo> does not have any bar method}

L'idea generale è quella di mettere la variabile in una scatola, e l'unica cosa che puoi fare è decomprimerla, preferibilmente arruolando l'aiuto del compilatore come proposto per Eiffel.

Haskell ce l'ha da zero ( Maybe), in C ++ puoi sfruttare boost::optional<T>ma puoi comunque ottenere un comportamento indefinito ...


6
-1 per "leva"!
Marco C,

8
Ma sicuramente molti costrutti di programmazione comuni consentono un codice senza senso, no? La divisione per zero è (probabilmente) insensata, ma consentiamo comunque la divisione e speriamo che il programmatore si ricordi di verificare che il divisore sia diverso da zero (o di gestire l'eccezione).
Tim Goodman,

3
Io non sono certo quello che si intende per "da zero", ma Maybenon è incorporata nel linguaggio di base, la sua definizione sembra appena essere nel preludio di serie: data Maybe t = Nothing | Just t.
fredoverflow,

4
@FredOverflow: dal momento che il preludio è caricato di default, lo considererei "da zero", che Haskell ha un sistema di tipi abbastanza sorprendente da consentire a questa (e ad altre) cose da definire con le regole generali della lingua è solo un bonus :)
Matthieu M.

2
@TimGoodman Mentre la divisione per zero potrebbe non essere il miglior esempio per tutti i tipi di valori numerici (IEEE 754 definisce un risultato per la divisione per zero), nei casi in cui genererebbe un'eccezione, invece di avere valori di tipo Tdivisi per altri valori di tipo T, limiteresti l'operazione a valori di tipo Tdivisi per valori di tipo NonZero<T>.
kbolino,

8

E qual è l'alternativa?

Tipi opzionali e corrispondenza dei motivi. Dato che non conosco C #, ecco un pezzo di codice in un linguaggio immaginario chiamato Scala # :-)

Customer.GetByLastName("Goodman")    // returns Option[Customer]
match
{
    case Some(customer) =>
    Console.WriteLine(customer.FirstName + " " + customer.LastName + " is awesome!");

    case None =>
    Console.WriteLine("There was no customer named Goodman.  How lame!");
}

6

Il problema con i null è che i linguaggi che li consentono praticamente ti costringono a programmare difensivamente contro di esso. Ci vuole molto sforzo (molto più che cercare di usare if-block difensivi) per assicurarsi che

  1. gli oggetti che ti aspetti non siano nulli non sono mai mai nulli, e
  2. che i tuoi meccanismi difensivi affrontano effettivamente tutti i potenziali NPE in modo efficace.

Quindi, in effetti, i null finiscono per essere una cosa costosa da avere.


1
Potrebbe essere utile se includessi un esempio in cui è necessario uno sforzo maggiore per gestire i valori null. Nel mio esempio, ammesso che sia troppo semplificato, lo sforzo è più o meno lo stesso in entrambi i casi. Non sono in disaccordo con te, trovo solo esempi di codice (pseudo) specifici che dipingono un quadro più chiaro delle dichiarazioni generali.
Tim Goodman,

2
Ah, non mi sono spiegato chiaramente. Non è che sia più difficile gestire i null. È estremamente difficile (se non impossibile) garantire che tutti gli accessi a riferimenti potenzialmente nulli siano già protetti. Cioè, in un linguaggio che consente riferimenti null, è semplicemente impossibile garantire che un pezzo di codice di dimensioni arbitrarie sia privo di eccezioni puntatore null, non senza alcune grandi difficoltà e sforzi. Questo è ciò che rende i riferimenti null una cosa costosa. È estremamente costoso garantire la completa assenza di eccezioni del puntatore null.
luis.espinal,

4

Il problema fino a che punto il tuo linguaggio di programmazione tenta di dimostrare la correttezza del tuo programma prima che venga eseguito. In una lingua tipicamente statica dimostri di avere i tipi corretti. Passando al valore predefinito di riferimenti non annullabili (con riferimenti annullabili facoltativi) è possibile eliminare molti dei casi in cui viene passato null e non dovrebbe esserlo. La domanda è se lo sforzo extra nella gestione di riferimenti non annullabili valga il vantaggio in termini di correttezza del programma.


4

Il problema non è tanto nullo, è che non è possibile specificare un tipo di riferimento non nullo in molte lingue moderne.

Ad esempio, il tuo codice potrebbe apparire come

public void MakeCake(Egg egg, Flour flour, Milk milk)
{
    if (egg == null) { throw ... }
    if (flour == null) { throw ... }
    if (milk == null) { throw ... }

    egg.Crack();
    MixingBowl.Mix(egg, flour, milk);
    // etc
}

// inside Mixing bowl class
public void Mix(Egg egg, Flour flour, Milk milk)
{
    if (egg == null) { throw ... }
    if (flour == null) { throw ... }
    if (milk == null) { throw ... }

    //... etc
}

Quando i riferimenti di classe vengono passati in giro, la programmazione difensiva ti incoraggia a controllare nuovamente tutti i parametri per null, anche se li hai controllati per null subito prima, specialmente quando costruisci unità riutilizzabili sotto test. Lo stesso riferimento potrebbe essere facilmente verificato per null 10 volte attraverso la base di codice!

Non sarebbe meglio se si potesse avere un tipo nullable normale quando si ottiene la cosa, si tratta di null allora e lì, quindi passarlo come tipo non nullable a tutte le funzioni e le classi del piccolo aiutante senza controllare null tutto tempo?

Questo è il punto della soluzione Option: non consentire all'utente di attivare gli stati di errore, ma di consentire la progettazione di funzioni che implicitamente non possono accettare argomenti nulli. Se l'implementazione "predefinita" dei tipi è nulla o non annullabile è meno importante della flessibilità di avere entrambi gli strumenti disponibili.

http://twistedoakstudios.com/blog/Post330_non-nullable-types-vs-c-fixing-the-billion-dollar-mistake

Questa è anche elencata come la funzione più votata n. 2 per C # -> https://visualstudio.uservoice.com/forums/121579-visual-studio/category/30931-languages-c


Penso che un altro punto importante sull'opzione <T> sia che è una monade (e quindi anche un endofunctor), quindi puoi comporre calcoli complessi su flussi di valori contenenti tipi opzionali senza controllare esplicitamente Noneogni passaggio.
Sara

3

Null è male. Tuttavia, la mancanza di un null può essere un male maggiore.

Il problema è che nel mondo reale hai spesso una situazione in cui non hai dati. Il tuo esempio di versione senza null può ancora esplodere: o hai fatto un errore logico e hai dimenticato di controllare Goodman o forse Goodman si è sposato tra quando hai controllato e quando l'hai cercata. (Aiuta a valutare la logica se pensi che Moriarty stia guardando ogni parte del tuo codice e cerchi di farti inciampare.)

Cosa fa la ricerca di Goodman quando non c'è? Senza un null devi restituire una sorta di cliente predefinito - e ora vendi roba a quel valore predefinito.

Fondamentalmente, dipende dal fatto che sia più importante che il codice funzioni indipendentemente da cosa o che funzioni correttamente. Se un comportamento improprio è preferibile a nessun comportamento, non si desidera null. (Per un esempio di come questa potrebbe essere la scelta giusta, prendere in considerazione il primo lancio di Ariane V. Un errore non rilevato / 0 ha provocato una brusca virata del razzo e l'autodistruzione ha sparato quando si è rotto a causa di questo. Il valore stava provando a calcolare in realtà non serviva più a nulla nello stesso istante in cui il booster era acceso - avrebbe fatto orbita nonostante la normale spazzatura di ritorno.)

Almeno 99 volte su 100 sceglierei di utilizzare i valori null. Vorrei saltare di gioia alla nota versione di sé, però.


1
Questa è una buona risposta Il costrutto null in programmazione non è il problema, sono tutte le istanze di valori null che lo sono. Se crei un oggetto predefinito, alla fine tutti i tuoi null verranno sostituiti con quelli predefiniti e la tua UI, DB e ovunque nel mezzo saranno riempiti con quelli, che probabilmente non sono più utili. Ma dormirai di notte perché almeno il tuo programma non si blocca, immagino.
Chris McCall,

2
Supponi che nullsia l'unico (o anche preferibile) modo di modellare l'assenza di un valore.
Sara

1

Ottimizza per il caso più comune.

Dover controllare sempre null è noioso: vuoi essere in grado di ottenere l'oggetto del cliente e lavorarci sopra.

Nel caso normale, questo dovrebbe funzionare bene: fai lo sguardo, ottieni l'oggetto e lo usi.

Nel caso eccezionale , in cui stai (casualmente) stai cercando un cliente per nome, senza sapere se quel record / oggetto esiste, avresti bisogno di qualche indicazione che questo non è riuscito. In questa situazione, la risposta è lanciare un'eccezione RecordNotFound (o lasciare che il provider SQL sottostante lo faccia per te).

Se ti trovi in ​​una situazione in cui non sai se puoi fidarti dei dati che arrivano (il parametro), forse perché sono stati inseriti da un utente, allora puoi anche fornire il "TryGetCustomer (nome, cliente esterno)" modello. Riferimenti incrociati con int.Parse e int. TryParse.


Asserirei che non si sa mai se ci si può fidare dei dati che arrivano perché entrano in una funzione ed è un tipo nullable. Il codice potrebbe essere sottoposto a refactoring per rendere l'input totalmente diverso. Quindi se c'è un nullo possibile devi sempre controllare. Quindi si finisce con un sistema in cui ogni variabile viene verificata per null ogni volta prima di accedervi. I casi in cui non è selezionato diventano la percentuale di errore per il tuo programma.
Chris McCall,

0

Le eccezioni non sono valutate!

Sono lì per un motivo. Se il tuo codice funziona male, c'è un modello geniale, chiamato eccezione , che ti dice che qualcosa non va.

Evitando di usare oggetti null nascondi parte di tali eccezioni. Non sto parlando dell'esempio OP in cui converte l'eccezione del puntatore null in un'eccezione ben tipizzata, questo potrebbe effettivamente essere una buona cosa poiché aumenta la leggibilità. Tuttavia, quando prendi la soluzione del tipo di Opzione , come sottolineato da @Jonas, nascondi le eccezioni.

Che nella tua applicazione di produzione, invece dell'eccezione da lanciare quando fai clic sul pulsante per selezionare un'opzione vuota, non succede nulla. Invece dell'eccezione del puntatore null verrebbe generata e probabilmente avremmo un rapporto di quell'eccezione (come in molte applicazioni di produzione abbiamo un tale meccanismo) e di quanto potremmo effettivamente risolverlo.

Rendere il tuo codice a prova di proiettile evitando le eccezioni è una cattiva idea, farti codice a prova di proiettile correggendo l'eccezione, questo è il percorso che sceglierei.


1
Ora so molto di più sul tipo di Opzione rispetto a quando ho pubblicato la domanda qualche anno fa, dato che ora li uso regolarmente a Scala. Il punto dell'opzione è che rende l'assegnazione Nonea qualcosa che non è un'opzione in un errore di compilazione e allo stesso modo usare un Optioncome se avesse un valore senza prima verificare che sia un errore di compilazione. Gli errori di compilazione sono sicuramente più belli delle eccezioni di runtime.
Tim Goodman,

Ad esempio: non puoi dire s: String = None, devi dire s: Option[String] = None. E se sè un'opzione, non puoi dirlo s.length, tuttavia puoi verificare che abbia un valore ed estrarre il valore allo stesso tempo, con la corrispondenza del modello:s match { case Some(str) => str.length; case None => throw RuntimeException("Where's my value?") }
Tim Goodman

Purtroppo in Scala si può ancora dire s: String = null, ma questo è il prezzo dell'interoperabilità con Java. In genere non consentiamo questo genere di cose nel nostro codice Scala.
Tim Goodman,

2
Tuttavia sono d'accordo con il commento generale sul fatto che le eccezioni (usate correttamente) non sono una cosa negativa, e "riparare" un'eccezione ingoiandola è generalmente un'idea terribile. Ma con il tipo Opzione, l'obiettivo è trasformare le eccezioni in errori di compilazione, che sono una cosa migliore.
Tim Goodman,

@TimGoodman è scala solo tattica o può essere utilizzato in altri linguaggi come Java e ActionScript 3? Inoltre sarebbe bello se potessi modificare la tua risposta con l'esempio completo.
Ilya Gazman,

0

Nullssono problematici perché devono essere esplicitamente controllati, ma il compilatore non è in grado di avvisarti che hai dimenticato di controllarli. Solo un'analisi statica che richiede tempo può dirlo. Fortunatamente, ci sono diverse buone alternative.

Porta la variabile fuori dal campo di applicazione. Troppo spesso, nullviene utilizzato come segnaposto quando un programmatore dichiara una variabile troppo presto o la mantiene troppo a lungo. L'approccio migliore è semplicemente quello di sbarazzarsi della variabile. Non dichiararlo fino a quando non hai un valore valido da inserire. Questa non è una limitazione così difficile come potresti pensare, e rende il tuo codice molto più pulito.

Usa il modello di oggetti null. A volte, un valore mancante è uno stato valido per il tuo sistema. Il modello di oggetti null è una buona soluzione qui. Evita la necessità di controlli espliciti costanti, ma consente comunque di rappresentare uno stato null. Non si tratta di "documentare una condizione di errore", come affermano alcune persone, perché in questo caso uno stato null è uno stato semantico valido. Detto questo, non dovresti usare questo modello quando uno stato null non è uno stato semantico valido. Dovresti semplicemente togliere la tua portata dall'ambito di applicazione.

Usa un'opzione Forse /. Innanzitutto, ciò consente al compilatore di avvisare che è necessario verificare la presenza di un valore mancante, ma non si limita a sostituire un tipo di controllo esplicito con un altro. Usando Options, puoi concatenare il tuo codice e continuare come se il tuo valore esistesse, senza la necessità di controllare fino all'ultimo minuto assoluto. In Scala, il tuo codice di esempio sarebbe simile a:

val customer = customerDb getByLastName "Goodman"
val awesomeMessage =
  customer map (c => s"${c.firstName} ${c.lastName} is awesome!")
val notFoundMessage = "There was no customer named Goodman.  How lame!"
println(awesomeMessage getOrElse notFoundMessage)

Nella seconda riga, dove stiamo costruendo il fantastico messaggio, non controlliamo esplicitamente se il cliente è stato trovato e la bellezza è che non è necessario . Non è fino all'ultima riga, che potrebbe essere molte righe più tardi, o anche in un modulo diverso, in cui ci preoccupiamo esplicitamente di ciò che accade se lo Optionè None. Non solo, se avessimo dimenticato di farlo, non avrebbe controllato il tipo. Anche allora, è fatto in modo molto naturale, senza una ifdichiarazione esplicita .

Contrastalo con un null, dove digita i controlli bene, ma dove devi fare un controllo esplicito su ogni passaggio del percorso o l'intera applicazione esplode in fase di esecuzione, se sei fortunato durante un caso d'uso che i test dell'unità esercitano. Non vale la seccatura.


0

Il problema centrale di NULL è che rende il sistema inaffidabile. Nel 1980 Tony Hoare nel saggio dedicato al suo Turing Award scrisse:

E così, il migliore dei miei consigli agli autori e ai progettisti di ADA è stato ignorato. .... Non permettere che questo linguaggio allo stato attuale sia utilizzato in applicazioni in cui l'affidabilità è fondamentale, ad esempio centrali nucleari, missili da crociera, sistemi di allarme rapido, sistemi di difesa antimissile contro i missili. Il prossimo razzo che andrà fuori strada a causa di un errore del linguaggio di programmazione potrebbe non essere un razzo spaziale esplorativo in un innocuo viaggio a Venere: potrebbe essere una testata nucleare che esplode su una delle nostre città. Un linguaggio di programmazione inaffidabile che genera programmi inaffidabili costituisce un rischio molto maggiore per il nostro ambiente e la nostra società rispetto alle automobili non sicure, ai pesticidi tossici o agli incidenti nelle centrali nucleari. Siate vigili per ridurre il rischio, non per aumentarlo.

Il linguaggio ADA è cambiato molto da allora, tuttavia esistono ancora problemi di questo tipo in Java, C # e molte altre lingue popolari.

È dovere dello sviluppatore creare contratti tra un cliente e un fornitore. Ad esempio, in C #, come in Java, è possibile utilizzare Genericsper ridurre al minimo l'impatto del Nullriferimento creando in sola lettura NullableClass<T>(due opzioni):

class NullableClass<T>
{
     public HasValue {get;}
     public T Value {get;}
}

e poi usalo come

NullableClass<Customer> customer = dbRepository.GetCustomer('Mr. Smith');
if(customer.HasValue){
  // one logic with customer.Value
}else{
  // another logic
} 

o usa due opzioni di stile con metodi di estensione C #:

customer.Do(
      // code with normal behaviour
      ,
      // what to do in case of null
) 

La differenza è significativa Come cliente di un metodo sai cosa aspettarti. Una squadra può avere la regola:

Se una classe non è di tipo NullableClass, la sua istanza non deve essere nulla .

Il team può rafforzare questa idea utilizzando Design by Contract e controllo statico al momento della compilazione, ad esempio con il presupposto:

function SaveCustomer([NotNullAttribute]Customer customer){
     // there is no need to check whether customer is null 
     // it is a client problem, not this supplier
}

o per una stringa

function GetCustomer([NotNullAndNotEmptyAttribute]String customerName){
     // there is no need to check whether customerName is null or empty 
     // it is a client problem, not this supplier
}

Questo approccio può aumentare drasticamente l' affidabilità delle applicazioni e la qualità del software. Design by Contract è un caso di logica Hoare , che fu popolato da Bertrand Meyer nel suo famoso libro sulla costruzione di software orientato agli oggetti e il linguaggio Eiffel nel 1988, ma non è utilizzato in modo non valido nella moderna produzione di software.


0

No.

L'incapacità di una lingua di tenere conto di un valore speciale come nullè il problema della lingua. Se guardi lingue come Kotlin o Swift , che costringono lo scrittore a gestire esplicitamente la possibilità di un valore nullo ad ogni incontro, allora non c'è pericolo.

In lingue come quella in questione, la lingua consente nulldi essere un valore di qualsiasi tipo -ish , ma non fornisce alcun costrutto per riconoscerlo in seguito, il che porta a cose nullinaspettatamente (specialmente quando passate da qualche altra parte), e così si ottengono arresti anomali. Questo è il male: lasciarti usare nullsenza farti gestire.


-1

Fondamentalmente l'idea centrale è che i problemi di riferimento puntatore null dovrebbero essere scoperti in fase di compilazione anziché in fase di esecuzione. Quindi, se stai scrivendo una funzione che accetta un oggetto e non vuoi che nessuno lo chiami con riferimenti null, allora il sistema di tipo dovrebbe permetterti di specificare quel requisito in modo che il compilatore possa usarlo per garantire che la chiamata alla tua funzione non sarebbe mai compilare se il chiamante non soddisfa le proprie esigenze. Questo può essere fatto in molti modi, ad esempio decorando i tuoi tipi o usando tipi di opzioni (chiamati anche tipi nullable in alcune lingue) ma ovviamente è molto più lavoro per i progettisti di compilatori.

Penso che sia comprensibile il motivo per cui negli anni '60 e '70, il progettista di compilatori non è andato all-in per implementare questa idea perché le macchine erano limitate nelle risorse, i compilatori dovevano essere mantenuti relativamente semplici e nessuno sapeva davvero quanto sarebbe andata male 40 anni lungo la linea.


-1

Ho una semplice obiezione contro null:

Rompe completamente la semantica del tuo codice introducendo ambiguità .

Spesso ti aspetti che il risultato sia di un certo tipo. Questo è semanticamente corretto. Ad esempio, hai chiesto al database un utente con un certo id, ti aspetti che il risultato sia di un certo tipo (= user). Ma cosa succede se non esiste un utente di quell'ID? Una (cattiva) soluzione è: aggiungere un nullvalore. Quindi il risultato è ambiguo : o è un usero lo è null. Ma nullnon è il tipo previsto. Ed è qui che inizia l' odore del codice .

Per evitare null, rendi il tuo codice semanticamente chiaro.

Ci sono sempre modi per aggirare null: refactoring in collezioni Null-objects, optionalsecc.


-2

Se sarà semanticamente possibile accedere a una posizione di memoria di riferimento o di tipo puntatore prima che il codice sia stato eseguito per calcolare un valore per esso, non c'è alcuna scelta perfetta su cosa dovrebbe accadere. Avere tali posizioni predefinite su riferimenti a puntatori nulli che possono essere letti e copiati liberamente, ma che guasteranno se riferiti o indicizzati, è una scelta possibile. Altre scelte includono:

  1. Hanno posizioni predefinite su un puntatore nullo, ma non consentono al codice di guardarle (o addirittura determinare che sono null) senza crash. Forse fornire un mezzo esplicito per impostare un puntatore su null.
  2. Hanno posizioni predefinite su un puntatore nullo e si arrestano in modo anomalo se si tenta di leggerne uno, ma forniscono un mezzo non crash per verificare se un puntatore contiene null. Fornisci anche un mezzo esplicito per impostare un puntatore su null.
  3. Hanno posizioni predefinite su un puntatore a qualche particolare istanza predefinita fornita dal compilatore del tipo indicato.
  4. Hanno posizioni predefinite su un puntatore a qualche particolare istanza predefinita fornita dal programma del tipo indicato.
  5. Avere qualsiasi accesso a puntatore null (non solo operazioni di dereferenziazione) chiama una routine fornita dal programma per fornire un'istanza.
  6. Progetta la semantica del linguaggio in modo tale che non esistano raccolte di posizioni di archiviazione, ad eccezione di un temporaneo compilatore inaccessibile, fino a quando le routine di inizializzazione non saranno eseguite su tutti i membri (qualcosa come un costruttore di array dovrebbe essere fornito con un'istanza predefinita, una funzione che restituirebbe un'istanza, o possibilmente una coppia di funzioni, una per restituire un'istanza e una che verrebbe chiamata su istanze precedentemente costruite se si verifica un'eccezione durante la costruzione dell'array).

L'ultima scelta potrebbe avere un certo fascino, specialmente se una lingua includesse tipi sia nullable che non nullable (si potrebbero chiamare i costruttori di array speciali per qualsiasi tipo, ma si può solo chiamarli quando si creano array di tipi non nullable), ma probabilmente non sarebbe stato possibile nel periodo in cui sono stati inventati i puntatori null. Tra le altre scelte, nessuna sembra più allettante di consentire la copia di puntatori null ma non la loro referenza o indicizzazione. L'approccio n. 4 potrebbe essere conveniente avere come opzione e dovrebbe essere abbastanza economico da implementare, ma non dovrebbe certamente essere l'unica opzione. Richiedere che i puntatori debbano puntare di default a un particolare oggetto valido è molto peggio che avere i puntatori di default su un valore nullo che può essere letto o copiato ma non referenziato o indicizzato.


-2

Sì, NULL è un design terribile , nel mondo orientato agli oggetti. In breve, l'utilizzo di NULL porta a:

  • gestione degli errori ad hoc (anziché eccezioni)
  • semantico ambiguo
  • lento invece di fallimento veloce
  • pensiero del computer vs. pensiero dell'oggetto
  • oggetti mutabili e incompleti

Controlla questo post del blog per una spiegazione dettagliata: http://www.yegor256.com/2014/05/13/why-null-is-bad.html

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.