Perché i riferimenti null evitati mentre si generano eccezioni sono considerati ok?


21

Non capisco del tutto il bashing coerente di riferimenti null da parte di alcune persone del linguaggio di programmazione. Cosa c'è di così brutto in loro? Se richiedo l'accesso in lettura a un file che non esiste, sono perfettamente felice di ottenere un'eccezione o un riferimento null e tuttavia le eccezioni sono considerate buone ma i riferimenti null sono considerati cattivi. Qual è il ragionamento alla base di questo?



2
Alcune lingue si bloccano su null meglio di altre. Per un "codice gestito", come .Net / Java, un riferimento null è solo un altro tipo di problema, mentre un altro codice nativo potrebbe non gestirlo con garbo (non hai menzionato una lingua specifica). Anche in un mondo gestito, a volte si desidera scrivere codice fail-safe (incorporato ?, armi?), E talvolta si desidera fare una puttana forte al più presto (unit test). Entrambi i tipi di codice potrebbero essere chiamati nella stessa libreria, il che rappresenterebbe un problema. In generale, penso che il codice che cerca di non danneggiare i sentimenti dei computer sia una cattiva idea. La sicurezza dei guasti è comunque DURA.
Giobbe

@Job: si tratta di sostenere la pigrizia. Se sai come gestire un'eccezione, la gestisci. A volte tale gestione potrebbe comportare il lancio di un'altra eccezione, ma non si dovrebbe mai lasciare che un'eccezione di riferimento null non venga gestita. Mai. Questo è l'incubo di ogni programmatore di manutenzione; è l'eccezione più inutile dell'intero albero. Basta chiedere Stack Overflow .
Aaronaught

o per dirla in altro modo: perché non restituire un codice di qualche tipo per rappresentare le informazioni di errore. Questa discussione infurierà per gli anni a venire.
gbjbaanb,

Risposte:


24

I riferimenti null non sono "evitati" più delle eccezioni, almeno da chiunque abbia mai conosciuto o letto. Penso che tu abbia frainteso la saggezza convenzionale.

Ciò che è male è un tentativo di accedere a un riferimento null (o di dereference un puntatore null, ecc.). Questo è male perché indica sempre un bug; non avresti mai fare qualcosa di simile di proposito, e se si sta facendo di proposito, allora questo è ancora peggio, perché è il che rende impossibile distinguere comportamento previsto dal comportamento buggy.

Ci sono alcuni gruppi marginali là fuori che odiano davvero il concetto di nullità per qualche motivo, ma come sottolinea Ed , se non lo hai nullo nildovrai semplicemente sostituirlo con qualcos'altro, che potrebbe portare a qualcosa peggio di un incidente (come la corruzione dei dati).

Molte strutture, infatti, abbracciano entrambi i concetti; ad esempio, in .NET, un modello frequente che vedrai è una coppia di metodi, uno preceduto dalla parola Try(come TryGetValue). Nel Trycaso, il riferimento viene impostato sul valore predefinito (in genere null) e, nell'altro caso, viene generata un'eccezione. Non c'è niente di sbagliato in nessuno dei due approcci; entrambi sono usati frequentemente in ambienti che li supportano.

Dipende davvero tutto dalla semantica. Se nullè un valore di ritorno valido, come nel caso generale della ricerca in una raccolta, quindi restituire null. D'altra parte, se è non è un valore di ritorno valida - per esempio, la ricerca di un record utilizzando una chiave primaria che è venuto dal proprio database - per poi tornare nullsarebbe una cattiva idea, perché il chiamante non sarà aspettava e, probabilmente, non lo controlla.

È davvero molto semplice capire quale semantico usare: ha senso che il risultato di una funzione sia indefinito? In tal caso, è possibile restituire un riferimento null. In caso contrario, genera un'eccezione.


5
In realtà ci sono lingue che non hanno null o zero e non "hanno bisogno di sostituirlo con qualcos'altro". Riferimenti nullabili implicano che potrebbe esserci qualcosa lì, o potrebbe non esserci. Se hai semplicemente bisogno che l'utente controlli esplicitamente se c'è qualcosa lì, hai risolto il problema. Vedi haskell per un esempio di vita reale.
Johanna Larsson

3
@ErikKronberg, sì, l '"errore del miliardo di dollari" e tutte quelle sciocchezze, la sfilata di persone che la escogita e afferma che è fresca e affascinante non finisce mai, ecco perché il precedente thread di commenti è stato eliminato. Queste sostituzioni rivoluzionarie che le persone non mancano mai di far apparire sono sempre una variante dell'Oggetto Null, dell'Opzione o del Contratto, che in realtà non eliminano magicamente l'errore logico sottostante, ma lo rimandano o promuovono rispettivamente. In ogni caso, questo è ovviamente una domanda su linguaggi di programmazione che fare hanno null, quindi in realtà, Haskell è piuttosto irrilevante in questa sede.
Aaronaught,

1
stai seriamente sostenendo che non richiedere un test per null è buono come richiederlo?
Johanna Larsson,

3
@ErikKronberg: Sì, sto "seriamente discutendo" che dover testare null non è particolarmente diverso dal (a) dover progettare ogni livello di un'applicazione attorno al comportamento di un oggetto null, (b) dover corrispondere al modello Opzioni per tutto il tempo, oppure (c) che non consentono alla persona chiamata di dire "Non lo so" e forzare un'eccezione o un arresto anomalo. C'è un motivo per cui nullè durato così a lungo per così tanto tempo e la maggior parte delle persone che dicono altrimenti sembrano essere accademici con poca esperienza nella progettazione di app del mondo reale con vincoli come requisiti incompleti o eventuale coerenza.
Aaronaught

3
@Aaronaught: A volte vorrei che ci fosse un pulsante di voto negativo per i commenti. Non c'è motivo di sfogare in quel modo.
Michael Shaw,

11

La grande differenza è che se si lascia fuori il codice per gestire i NULL, il codice continuerà molto probabilmente a bloccarsi in un secondo momento con un messaggio di errore non correlato, dove come per le eccezioni, l'eccezione verrebbe sollevata nel punto iniziale di errore (apertura un file per la lettura nel tuo esempio).


4
La mancata gestione di un NULL sarebbe intenzionale ignoranza dell'interfaccia tra il tuo codice e qualunque cosa lo restituisse. Gli sviluppatori che commetterebbero questo errore renderebbero gli altri in una lingua che non fa NULL.
Blrfl,

@Blrfl, idealmente i metodi sono piuttosto brevi, quindi è facile capire esattamente dove si sta verificando il problema. Un buon debugger può in genere cercare bene un'eccezione di riferimento null, anche se il codice è lungo. Cosa dovrei fare se sto cercando di leggere un'impostazione critica da un registro, che non è presente? Il mio registro è incasinato e sono più sicuro di fallire e infastidire l'utente, piuttosto che ricreare silenziosamente un nodo e impostarlo sul valore predefinito. E se un virus avesse fatto questo? Quindi, se ottengo un null, dovrei lanciare un'eccezione specializzata o lasciarlo strappare? Con metodi brevi qual è la grande differenza?
Giobbe

@Job: cosa succede se non si dispone di un debugger? Ti rendi conto che il 99,99% delle volte l'applicazione verrà eseguita in un ambiente di rilascio? Quando ciò accade, vorrai aver usato un'eccezione più significativa. È possibile che la tua app debba ancora fallire e infastidire l'utente, ma almeno fornirà informazioni di debug che ti permetteranno di rintracciare prontamente il problema, mantenendo al minimo tale fastidio.
Aaronaught

@Birfl, a volte non voglio gestire il caso in cui un valore null sarebbe naturale tornare. Ad esempio, supponiamo che io abbia un contenitore che associa le chiavi ai valori. Se la mia logica garantisce che non provo mai a leggere i valori che non ho prima memorizzato, non dovrei mai restituire null. In tal caso, preferirei di gran lunga avere un'eccezione che fornisce quante più informazioni possibili per indicare cosa è andato storto, piuttosto che restituire un null per fallire misteriosamente in qualche altra parte del programma.
Winston Ewert,

Per dirla in altro modo, con l'eccezione, devo gestire esplicitamente il caso insolito, altrimenti il ​​mio programma muore immediatamente. Con riferimenti null se il codice non gestisce esplicitamente il caso insolito, proverà a zoppicare. Penso che sia meglio prendere il caso fallito ora.
Winston Ewert,

8

Perché i valori null non sono una parte necessaria di un linguaggio di programmazione e sono una fonte coerente di bug. Come dici tu, l'apertura di un file può comportare un errore, che può essere comunicato come valore di ritorno nullo o tramite un'eccezione. Se valori nulli non erano consentiti, esiste un modo coerente e singolare per comunicare l'errore.

Inoltre, questo non è il problema più comune con i null. Molte persone ricordano di verificare la presenza di null dopo aver chiamato una funzione che potrebbe restituirla. Il problema affiora molto di più nella tua progettazione consentendo alle variabili di essere nulle in vari punti dell'esecuzione del tuo programma. Puoi progettare il tuo codice in modo tale che valori null non siano mai consentiti, ma se null non fosse consentito a livello di lingua, nulla di tutto ciò sarebbe necessario.

Tuttavia, in pratica avresti ancora bisogno di un modo per indicare se una variabile è o non è inizializzata. Avresti quindi una forma di bug in cui il tuo programma non si arresta in modo anomalo, ma continua invece a utilizzare un valore predefinito eventualmente non valido. Onestamente non so quale sia la migliore. Per i miei soldi mi piace schiantarmi presto e spesso.


Considera una sottoclasse non inizializzata con una stringa identificativa e tutti i suoi metodi generano eccezioni. Se uno di questi si presenta, sai cosa è successo. Sui sistemi embedded con memoria limitata, la versione di produzione della factory non inizializzata può restituire null.
Jim Balter,

3
@Jim Balter: suppongo di essere confuso su come sarebbe davvero utile in pratica però. In qualsiasi programma non banale dovrai ad un certo punto affrontare valori che potrebbero non essere inizializzati. Quindi, ci deve essere un modo per indicare un valore predefinito. Pertanto, devi ancora verificarlo prima di continuare. Quindi, invece di un potenziale arresto anomalo, ora stai potenzialmente lavorando con dati non validi.
Ed S.

1
Sai anche cosa è successo quando ottieni nullcome valore di ritorno: le informazioni che hai richiesto non esistono. Non c'è alcuna differenza. In entrambi i casi, il chiamante deve convalidare il valore restituito se deve effettivamente utilizzare tali informazioni per un determinato scopo. Che si tratti di validare a null, un oggetto null o una monade non fa alcuna differenza pratica.
Aaronaught

1
@Jim Balter: Giusto, non vedo ancora la differenza pratica e non vedo come semplifichi la vita alle persone che scrivono programmi reali al di fuori del mondo accademico. Non significa che non ci siano benefici, semplicemente che non mi sembrano evidenti.
Ed S.

1
Ho spiegato la differenza pratica con una classe non inizializzata due volte - identifica l'origine dell'elemento null - rendendo possibile individuare il bug quando si verifica una dereference di null. Per quanto riguarda i linguaggi progettati attorno a paradigmi null-less, evitano il problema fin dall'inizio: forniscono modi alternativi di programmare, evitando variabili non inizializzate; che può sembrare difficile da capire se non si ha familiarità con loro. Anche la programmazione strutturata, che evita anche una grande classe di bug, una volta era considerata "accademica".
Jim Balter,

5

Tony Hoare, che ha creato l'idea di un riferimento nullo in primo luogo, lo chiama il suo errore di un milione di dollari .

Il problema non riguarda i riferimenti null di per sé, ma la mancanza di un adeguato controllo del tipo nella maggior parte dei linguaggi sicuri (altrimenti).

Questa mancanza di supporto, dal linguaggio, significa che i bug "null-bugs" potrebbero essere in agguato nel programma per molto tempo prima di essere rilevati. Tale è la natura dei bug, ovviamente, ma i "null-bug" sono ora noti per essere evitabili.

Questo problema è particolarmente presente in C o C ++ (ad esempio) a causa dell'errore "duro" che provoca (un arresto anomalo del programma, immediato, senza un ripristino elegante).

In altre lingue, c'è sempre la domanda su come gestirli.

In Java o C # si otterrebbe un'eccezione se si tenta di invocare un metodo su un riferimento null e potrebbe andare bene. E quindi la maggior parte dei programmatori Java o C # sono abituati a questo e non capiscono perché uno vorrebbe fare diversamente (e ridere di C ++).

In Haskell, devi fornire esplicitamente un'azione per il caso nullo, e quindi i programmatori Haskell si gongolano con i loro colleghi, perché hanno capito bene (giusto?).

È, in realtà, il vecchio dibattito sul codice di errore / eccezione, ma questa volta con un valore Sentinel al posto del codice di errore.

Come sempre, qualunque sia il più appropriato dipende in realtà dalla situazione e dalla semantica che stai cercando.


Precisamente. nullè davvero malvagio, perché sovverte il sistema dei tipi . (Certo, l'alternativa ha uno svantaggio nella sua verbosità, almeno in alcune lingue.)
Lumaca meccanica,

2

Non è possibile allegare un messaggio di errore leggibile dall'uomo a un puntatore null.

(Tuttavia, è possibile lasciare un messaggio di errore in un file di registro.)

In alcune lingue / ambienti che consentono l'aritmetica dei puntatori, se uno degli argomenti puntatore è nullo ed è consentito nel calcolo, il risultato sarebbe un invalido , non nullo puntatore. (*) Più potere per te.

(*) Questo succede molto nella programmazione COM , dove se provi a chiamare un metodo di interfaccia ma il puntatore all'interfaccia è nullo, si tradurrebbe in una chiamata a un indirizzo non valido che è quasi, ma non del tutto, esattamente diverso da zero.


2

Restituire NULL (o zero numerico o falso booleano) per segnalare un errore è errato sia tecnicamente che concettualmente.

Tecnicamente, stai caricando il programmatore controllando immediatamente il valore di ritorno , nel punto esatto in cui viene restituito. Se si aprono venti file di fila e la segnalazione di errore viene eseguita restituendo NULL, il codice che consuma deve controllare ogni file letto singolarmente ed uscire da eventuali loop e costrutti simili. Questa è una ricetta perfetta per il codice ingombra. Se, tuttavia, si sceglie di segnalare l'errore generando un'eccezione, il codice che consuma può scegliere di gestire immediatamente l'eccezione o lasciarlo salire a tutti i livelli appropriati, anche attraverso le chiamate di funzione. Questo rende il codice molto più pulito.

Concettualmente, se si apre un file e qualcosa va storto, restituire un valore (anche NULL) è sbagliato. Non hai nulla da restituire, perché l'operazione non è terminata. Restituire NULL è l'equivalente concettuale di "Ho letto con successo il file, ed ecco cosa contiene - niente". Se è quello che vuoi esprimere (cioè, se NULL ha senso come risultato effettivo per l'operazione in questione), allora restituisci NULL, ma se vuoi segnalare un errore, usa le eccezioni.

Storicamente, gli errori sono stati segnalati in questo modo perché i linguaggi di programmazione come C non hanno una gestione delle eccezioni incorporata nel linguaggio e il modo consigliato (usando salti lunghi) è un po 'peloso e un po' contro-intuitivo.

C'è anche un lato di manutenzione del problema: con le eccezioni, devi scrivere un codice extra per gestire l'errore; in caso contrario, il programma fallirà presto e con difficoltà (il che è positivo). Se si restituisce NULL per segnalare errori, il comportamento predefinito per il programma è di ignorare l'errore e proseguire, fino a quando non porta ad altri problemi lungo la strada: dati corrotti, segfaults, NullReferenceExceptions, a seconda della lingua. Per segnalare l'errore in anticipo e ad alta voce, devi scrivere un codice aggiuntivo e indovinare cosa: questa è la parte che viene lasciata fuori quando sei in una scadenza stretta.


1

Come già sottolineato, molte lingue non convertono la dereferenza di un puntatore null in un'eccezione accettabile. Farlo è un trucco relativamente moderno. Quando il problema del puntatore null è stato riconosciuto per la prima volta, le eccezioni non erano ancora state inventate.

Se si consentono puntatori null come caso valido, si tratta di un caso speciale. È necessaria una logica di gestione di casi speciali, spesso in molti luoghi diversi. Questa è una complessità in più.

Che si tratti di puntatori potenzialmente nulli o meno, se non si utilizzano i lanci di eccezione per gestire casi eccezionali, è necessario gestire tali casi eccezionali in altro modo. In genere, ogni chiamata di funzione deve disporre di controlli per quei casi eccezionali, sia per impedire che la chiamata di funzione venga chiamata in modo inappropriato, sia per rilevare il caso di errore alla chiusura della funzione. Questa è una complessità extra che può essere evitata usando le eccezioni.

Più complessità di solito significa più errori.

Le alternative all'utilizzo di puntatori null nelle strutture di dati (ad esempio per contrassegnare l'inizio / la fine di un elenco collegato) includono l'uso di elementi sentinella. Questi possono dare la stessa funzionalità con molta meno complessità. Tuttavia, ci possono essere altri modi per gestire la complessità. Un modo è avvolgere il puntatore potenzialmente nullo in una classe di puntatore intelligente in modo che i controlli null siano necessari solo in una posizione.

Cosa fare del puntatore null quando viene rilevato? Se non è possibile integrare la gestione dei casi eccezionali, è sempre possibile generare un'eccezione e delegare efficacemente la gestione dei casi speciali al chiamante. Ed è esattamente ciò che alcune lingue fanno per impostazione predefinita quando si dereferenzia un puntatore nullo.


1

Specifico per C ++, ma qui i riferimenti null sono evitati perché la semantica null in C ++ è associata ai tipi di puntatore. È abbastanza ragionevole che una funzione di apertura del file fallisca e restituisca un puntatore nullo; in effetti la fopen()funzione fa esattamente questo.


Infatti, se ti trovi con un riferimento null in C ++, è perché il tuo programma è già rotto .
Kaz Dragon

1

Dipende dalla lingua.

Ad esempio, Objective-C consente di inviare un messaggio a un oggetto null (zero) senza problemi. Anche una chiamata a zero restituisce zero ed è considerata una caratteristica della lingua.

Personalmente mi piace poiché puoi fare affidamento su quel comportamento ed evitare tutti quei if(obj == null)costrutti nidificati contorti .

Per esempio:

if (myObject != nil && [myObject doSomething])
{
    ...
}

Può essere abbreviato in:

if ([myObject doSomething])
{
    ...
}

In breve, rende il tuo codice più leggibile.


0

Non do un grido se restituisci un valore nullo o generi un'eccezione, purché tu lo documenti.


E anche il nome : GetAddressMaybeo GetSpouseNameOrNull.
rwong

-1

Un riferimento Null di solito si verifica perché qualcosa nella logica del programma è stato perso, ovvero: si è arrivati ​​a una riga di codice senza passare attraverso l'impostazione richiesta per quel blocco di codice.

D'altra parte, se si lancia un'eccezione per qualcosa, significa che si è riconosciuto che una particolare situazione potrebbe verificarsi durante il normale funzionamento del programma e che viene gestita.


2
Un riferimento null può "verificarsi" per un gran numero di ragioni, pochissime delle quali riguardano qualcosa che manca. Forse lo stai anche confondendo con un'eccezione di riferimento null .
Aaronaught

-1

I riferimenti null sono spesso molto utili: ad esempio, un elemento in un elenco collegato può avere un successore o nessun successore. L'uso nullper "nessun successore" è perfettamente naturale. Oppure, una persona può avere un coniuge o meno - usare nullper "la persona non ha un coniuge" è perfettamente naturale, molto più naturale di avere un valore speciale "senza coniuge" al quale il Person.Spousemembro potrebbe riferirsi.

Ma: molti valori non sono opzionali. In un tipico programma OOP, direi che oltre la metà dei riferimenti non può essere nulldopo l'inizializzazione, altrimenti il ​​programma fallirà. Altrimenti il ​​codice dovrebbe essere pieno di if (x != null)controlli. Quindi perché ogni riferimento dovrebbe essere nullable per impostazione predefinita? Dovrebbe davvero essere il contrario: le variabili dovrebbero essere non annullabili per impostazione predefinita e dovresti esplicitamente dire "oh, e anche questo valore può essere null".


c'è qualcosa da questa discussione che vorresti aggiungere di nuovo alla tua risposta? Questo thread di commenti si è riscaldato un po 'e vorremmo ripulirlo. Qualsiasi discussione estesa dovrebbe essere presa per chattare .

In mezzo a tutto quel battibecco ci potevano essere uno o due punti di interesse reale. Sfortunatamente non ho visto il commento di Mark fino a quando non ho annullato la discussione estesa. In futuro, contrassegnare la risposta per l'attenzione dei moderatori se si desidera conservare i commenti fino a quando non si ha il tempo di esaminarli e modificare la risposta in modo appropriato.
Josh K,

-1

La tua domanda è formulata in modo confuso. Intendi un'eccezione di riferimento null (che in realtà è causata da un tentativo di deriferimento null)? La chiara ragione per non voler quel tipo di eccezione è che non ti dà informazioni su cosa è andato storto, o anche quando - il valore avrebbe potuto essere impostato su null in qualsiasi punto del programma. Scrivi "Se richiedo l'accesso in lettura a un file che non esiste, sono perfettamente felice di ottenere un'eccezione o un riferimento null", ma non dovresti essere perfettamente felice di ottenere qualcosa che non fornisce alcuna indicazione della causa . In nessun punto della stringa "è stato fatto un tentativo di dereference null" si fa menzione della lettura o di file inesistenti. Forse vuoi dire che saresti perfettamente felice di ottenere null come valore di ritorno da una chiamata in lettura - è una questione abbastanza diversa, ma non ti dà ancora informazioni sul perché la lettura non è riuscita; E'


No, non intendo un'eccezione di riferimento null. In tutte le lingue che conosco le variabili non inizializzate ma dichiarate sono una forma di null.
davidk01,

Ma la tua domanda non riguardava le variabili non inizializzate. E ci sono linguaggi che non usano null per variabili non inizializzate, ma usano invece oggetti wrapper che possono eventualmente contenere un valore - Scala e Haskell, per esempio.
Jim Balter,

1
"... ma invece usa oggetti wrapper che possono eventualmente contenere un valore ." Che chiaramente non assomiglia a un tipo nullable .
Aaronaught,

1
In haskell non esiste una variabile non inizializzata. Potresti potenzialmente dichiarare uno IORef e seminarlo con None come valore iniziale, ma è praticamente l'analogo di dichiarare una variabile in qualche altra lingua e lasciarla non inizializzata che porta con sé tutti gli stessi problemi. Lavorando nel nucleo puramente funzionale al di fuori della IO monade, i programmatori haskell non fanno ricorso ai tipi di riferimento, quindi non vi è alcun problema di riferimento nullo.
davidk01,

1
Se hai un valore "Missing" eccezionale, è esattamente lo stesso di un valore "null" eccezionale - se hai gli stessi strumenti disponibili per gestirlo. Questo è un "se" significativo. Hai bisogno di ulteriore complessità per gestire quel caso in entrambi i modi. In Haskell, la corrispondenza dei modelli e il sistema di tipi forniscono un modo per aiutare a gestire quella complessità. Tuttavia, ci sono altri strumenti per gestire la complessità in altre lingue. Le eccezioni sono uno di questi strumenti.
Steve314,
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.