Se null è male, perché le lingue moderne lo implementano? [chiuso]


82

Sono sicuro che i progettisti di linguaggi come Java o C # conoscessero problemi relativi all'esistenza di riferimenti null (vedi I riferimenti null sono davvero una cosa negativa? ). Anche l'implementazione di un tipo di opzione non è molto più complessa dei riferimenti null.

Perché hanno deciso di includerlo comunque? Sono sicuro che la mancanza di riferimenti null incoraggerebbe (o addirittura forzerà) una migliore qualità del codice (specialmente una migliore progettazione delle librerie) sia da parte dei creatori di lingue che degli utenti.

È semplicemente a causa del conservatorismo: "ce l'hanno altre lingue, anche noi dobbiamo averlo ..."?


99
null è fantastico. Lo adoro e lo uso ogni giorno.
Pieter B,

17
@PieterB Ma lo usi per la maggior parte dei riferimenti o vuoi che la maggior parte dei riferimenti non sia nulla? L'argomento non è che non dovrebbero esserci dati nullable, ma solo che dovrebbero essere espliciti e opt-in.

11
@PieterB Ma quando la maggioranza non dovrebbe essere nullable, non avrebbe senso rendere l'abilità null l'eccezione piuttosto che l'impostazione predefinita? Si noti che mentre la consueta progettazione di tipi di opzione è quella di forzare il controllo esplicito per assenza e decompressione, si può anche avere la nota semantica Java / C # / ... per i riferimenti nullable opt-in (usare come se non nullable, saltare in aria se nullo). Almeno impedirebbe alcuni bug e renderebbe molto più pratica un'analisi statica che si lamenta della mancanza di controlli null.

20
Ragazzi, come va ragazzi? Di tutte le cose che possono andar male nel software, provare a dereferenziare un null non è affatto un problema. Genera SEMPRE un AV / segfault e quindi viene riparato. C'è così tanta carenza di bug che devi preoccuparti di questo? In tal caso, ne ho molti di riserva, e nessuno di loro risolve problemi con riferimenti / puntatori nulli.
Martin James,

13
@MartinJames "Genera SEMPRE un AV / segfault e quindi viene riparato" - no, no non lo fa.
detly

Risposte:


97

Disclaimer: Dal momento che non conosco personalmente alcun disegnatore di lingue, qualsiasi risposta che darò sarà speculativa.

Dallo stesso Tony Hoare :

Lo chiamo il mio errore da miliardi di dollari. Fu l'invenzione del riferimento nullo nel 1965. A quel tempo, stavo progettando il primo sistema di tipi completo per riferimenti in un linguaggio orientato agli oggetti (ALGOL W). Il mio obiettivo era quello di garantire che ogni uso dei riferimenti fosse assolutamente sicuro, con il controllo eseguito automaticamente dal compilatore. Ma non ho resistito alla tentazione di inserire un riferimento null, semplicemente perché era così facile da implementare. Ciò ha portato a innumerevoli errori, vulnerabilità e arresti anomali del sistema, che probabilmente hanno causato un miliardo di dollari di dolore e danni negli ultimi quarant'anni.

Enfasi mia.

Naturalmente non gli sembrava una cattiva idea in quel momento. E 'probabile che sia stato perpetuato, in parte per la stessa ragione - se mi sembrava una buona idea per l'inventore Turing Award-winning di quicksort, non è sorprendente che molte persone ancora non capiscono perché è il male. È anche probabile in parte perché è conveniente che le nuove lingue siano simili alle lingue più vecchie, sia per ragioni di marketing che di curva di apprendimento. Caso in questione:

"Inseguivamo i programmatori C ++. Siamo riusciti a trascinarne molti a metà strada verso Lisp." -Guy Steele, coautore delle specifiche Java

(Fonte: http://www.paulgraham.com/icad.html )

E, naturalmente, C ++ ha valore nullo perché C ha valore nullo e non è necessario entrare nell'impatto storico di C. In genere C # ha sostituito J ++, che era l'implementazione di Microsoft di Java, e ha anche sostituito C ++ come linguaggio di scelta per lo sviluppo di Windows, quindi avrebbe potuto ottenerlo da entrambi.

EDIT Ecco un'altra citazione da Hoare che vale la pena considerare:

Nel complesso, i linguaggi di programmazione sono molto più complicati di quanto non fossero in passato: l'orientamento agli oggetti, l'ereditarietà e altre caratteristiche non sono ancora stati pensati dal punto di vista di una disciplina coerente e scientificamente fondata o di una teoria della correttezza . Il mio postulato originale, che ho perseguito come scienziato per tutta la mia vita, è che si usano i criteri di correttezza come mezzo per convergere su un design del linguaggio di programmazione decente, uno che non crei trappole per i suoi utenti, e in cui che i diversi componenti del programma corrispondono chiaramente ai diversi componenti delle sue specifiche, quindi puoi ragionare in modo compositivo su di esso. [...] Gli strumenti, incluso il compilatore, devono basarsi su una teoria di cosa significhi scrivere un programma corretto. -Intervista di storia orale di Philip L. Frana, 17 luglio 2002, Cambridge, Inghilterra; Charles Babbage Institute, Università del Minnesota. [ Http://www.cbi.umn.edu/oh/display.phtml?id=343]

Ancora una volta, enfatizzare il mio. Sun / Oracle e Microsoft sono aziende e la linea di fondo di qualsiasi azienda è il denaro. I vantaggi che derivano dall'avere nullpotrebbero aver superato i contro, o potrebbero aver semplicemente avuto una scadenza troppo stretta per considerare pienamente il problema. Come esempio di un errore linguistico diverso che probabilmente si è verificato a causa delle scadenze:

È un peccato che Cloneable sia rotto, ma succede. Le API Java originali sono state eseguite molto rapidamente in tempi ristretti per soddisfare una finestra di mercato di chiusura. Il team Java originale ha fatto un lavoro incredibile, ma non tutte le API sono perfette. Clonabile è un punto debole e penso che le persone dovrebbero essere consapevoli dei suoi limiti. -Josh Bloch

(Fonte: http://www.artima.com/intv/bloch13.html )


32
Caro downvoter: come posso migliorare la mia risposta?
Doval,

6
In realtà non hai risposto alla domanda; hai fornito solo alcune citazioni su alcune opinioni post-fact-fact e alcune ulteriori agitazioni sul "costo". (Se null è un errore di un miliardo di dollari, i dollari risparmiati da MS e Java implementandolo riducono tale debito?)
DougM,

29
@DougM Cosa ti aspetti che faccia, colpire ogni designer di lingue degli ultimi 50 anni e chiedergli perché si è implementato nullnella sua lingua? Qualsiasi risposta a questa domanda sarà speculativa a meno che non provenga da un progettista di lingue. Non conosco nessuno che frequenti questo sito oltre a Eric Lippert. L'ultima parte è un'aringa rossa per numerosi motivi. La quantità di codice di terze parti scritta sopra le API di MS e Java ovviamente supera la quantità di codice nell'API stessa. Quindi, se i tuoi clienti vogliono null, dai loro null. Supponi anche che abbiano accettato il nullcosto.
Doval,

3
Se l'unica risposta che puoi dare è speculativa, specifica chiaramente nel tuo paragrafo iniziale. (Mi hai chiesto come potresti migliorare la tua risposta, e io ho risposto. Qualsiasi parentesi è solo un commento che puoi sentirti libero di ignorare; questo è ciò che le parentesi sono in inglese, dopo tutto.)
DougM,

7
Questa risposta è ragionevole; Ho aggiunto alcune altre considerazioni nella mia. Prendo atto che ICloneableè rotto in modo simile in .NET; sfortunatamente questo è un posto in cui le carenze di Java non sono state apprese in tempo.
Eric Lippert,

121

Sono sicuro che i progettisti di linguaggi come Java o C # conoscessero problemi relativi all'esistenza di riferimenti null

Ovviamente.

Anche l'implementazione di un tipo di opzione non è molto più complessa dei riferimenti null.

Mi permetto di dissentire! Le considerazioni di progettazione che sono state inserite in tipi di valore nulla in C # 2 erano complesse, controverse e difficili. Hanno portato i team di progettazione di entrambe le lingue e il tempo di esecuzione per molti mesi di dibattiti, implementazione di prototipi e così via, e in effetti la semantica del nulla boxing è stata cambiata molto vicino alla spedizione C # 2.0, il che è stato molto controverso.

Perché hanno deciso di includerlo comunque?

Tutta la progettazione è un processo di scelta tra molti obiettivi sottilmente e gravemente incompatibili; Posso solo dare un breve schizzo di alcuni dei fattori che sarebbero considerati:

  • L'ortogonalità delle caratteristiche del linguaggio è generalmente considerata una buona cosa. C # ha tipi di valore annullabili, tipi di valore non annullabili e tipi di riferimento annullabili. Non esistono tipi di riferimento non annullabili, il che rende il sistema di tipi non ortogonale.

  • La familiarità con gli utenti esistenti di C, C ++ e Java è importante.

  • La facile interoperabilità con COM è importante.

  • La facile interoperabilità con tutti gli altri linguaggi .NET è importante.

  • La facile interoperabilità con i database è importante.

  • La coerenza della semantica è importante; se abbiamo un riferimento a TheKingOfFrance uguale a null significa sempre "non esiste un Re di Francia in questo momento", oppure può significare anche "Esiste sicuramente un Re di Francia, ma non so chi sia adesso"? o può significare "l'idea stessa di avere un re in Francia è senza senso, quindi non fare nemmeno la domanda!"? Null può significare tutte queste cose e altro in C #, e tutti questi concetti sono utili.

  • Il costo della prestazione è importante.

  • Essere sensibili all'analisi statica è importante.

  • La coerenza del sistema di tipi è importante; possiamo sempre sapere che un riferimento non annullabile non è mai stato ritenuto non valido in nessuna circostanza? Che dire del costruttore di un oggetto con un campo non annullabile di tipo riferimento? Che dire del finalizzatore di un tale oggetto, in cui l'oggetto è finalizzato perché il codice che avrebbe dovuto compilare il riferimento ha generato un'eccezione ? Un sistema di tipo che ti mente sulle sue garanzie è pericoloso.

  • E la coerenza della semantica? I valori null si propagano quando vengono utilizzati, ma i riferimenti null generano eccezioni quando vengono utilizzati. Questo è incoerente; tale incoerenza è giustificata da qualche vantaggio?

  • Possiamo implementare la funzionalità senza rompere altre funzionalità? Quali altre possibili funzioni future la precluderanno?

  • Vai in guerra con l'esercito che hai, non quello che ti piacerebbe. Ricorda, C # 1.0 non aveva generici, quindi parlare Maybe<T>in alternativa è un completo non-principiante. .NET avrebbe dovuto scivolare per due anni mentre il team di runtime aggiungeva generici, al solo scopo di eliminare riferimenti null?

  • Che dire della coerenza del sistema di tipi? Puoi dire Nullable<T>qualsiasi tipo di valore: no, aspetta, è una bugia. Non si può dire Nullable<Nullable<T>>. Dovresti essere in grado di farlo? In tal caso, quali sono le sue semantiche desiderate? Vale la pena fare in modo che l'intero sistema di tipi contenga un caso speciale solo per questa funzione?

E così via. Queste decisioni sono complesse.


12
+1 per tutto ma soprattutto per far apparire generici. È facile dimenticare che ci sono stati periodi nella storia di Java e C # in cui i generici non esistevano.
Doval,

2
Forse una domanda stupida (sono solo un laureato IT) - ma non è stato possibile implementare il tipo di opzione a livello di sintassi (con CLR che non ne sa nulla) come riferimento normale nullable che richiede un controllo "di valore" prima di utilizzare in codice? Credo che i tipi di opzioni non necessitino di alcun controllo in fase di esecuzione.
mrpyo,

2
@mrpyo: Certo, questa è una possibile scelta di implementazione. Nessuna delle altre scelte progettuali scompare e quella scelta di implementazione ha molti vantaggi e svantaggi.
Eric Lippert,

1
@mrpyo Penso che forzare un controllo "di valore" non sia una buona idea. Teoricamente è un'ottima idea, ma in pratica, IMO porterebbe tutti i tipi di controlli vuoti, solo per soddisfare le eccezioni verificate del compilatore come in Java e le persone che lo ingannano con catchesquesto non fanno nulla. Penso che sia meglio far esplodere il sistema invece di continuare a funzionare in uno stato probabilmente non valido.
NothingsImpossible

2
@voo: le matrici di tipo di riferimento non annullabili sono difficili per molte ragioni. Esistono molte soluzioni possibili e tutte impongono costi su diverse operazioni. Il suggerimento di Supercat è di tracciare se un elemento può essere letto legalmente prima che sia assegnato, il che comporta costi. Il tuo è assicurarti che un inizializzatore venga eseguito su ogni elemento prima che l'array sia visibile, il che impone una diversa serie di costi. Quindi ecco il problema: non importa quale di queste tecniche si scelga, qualcuno si lamenterà che non è efficiente per il proprio scenario da compagnia. Questo è un aspetto grave rispetto alla funzionalità.
Eric Lippert,

28

Null ha uno scopo molto valido di rappresentare una mancanza di valore.

Dirò che sono la persona più vocale che conosco riguardo agli abusi di nullità e a tutti i mal di testa e le sofferenze che possono causare specialmente se usati liberamente.

La mia posizione personale è che le persone possono usare valori null solo quando possono giustificare che è necessario e appropriato.

Esempio che giustifica i null:

La data della morte è in genere un campo nullable. Esistono tre possibili situazioni con data di morte. O la persona è morta e la data è nota, la persona è morta e la data è sconosciuta o la persona non è morta e quindi non esiste una data di morte.

Date of Death è anche un campo DateTime e non ha un valore "sconosciuto" o "vuoto". Ha la data predefinita che appare quando si crea un nuovo datetime che varia in base alla lingua utilizzata, ma c'è tecnicamente la possibilità che una persona in realtà muoia in quel momento e si contrassegna come "valore vuoto" se si dovesse usa la data predefinita.

I dati dovrebbero rappresentare correttamente la situazione.

La persona è morta è nota la data di morte (09/03/1984)

Semplice, "3/9/1984"

La persona è morta la data di morte non è nota

Quindi cosa c'è di meglio? Null , '0/0/0000' o '01 / 01/1869 '(o qualunque sia il tuo valore predefinito?)

La persona non è morta la data di morte non è applicabile

Quindi cosa c'è di meglio? Null , '0/0/0000' o '01 / 01/1869 '(o qualunque sia il tuo valore predefinito?)

Quindi pensiamo che ogni valore su ...

  • Null , ha implicazioni e preoccupazioni è necessario essere cauti, accidentalmente cercando di manipolare senza confermare che non è nulla prima per esempio sarebbe un'eccezione, ma è anche meglio rappresenta situazione reale ... Se la persona non è morto la data della morte non esiste ... non è niente ... è nulla ...
  • 0/0/0000 , questo potrebbe andare bene in alcune lingue e potrebbe anche essere una rappresentazione appropriata senza data. Sfortunatamente alcune lingue e convalide lo rifiuteranno come un datetime non valido che lo rende un non andare in molti casi.
  • 1/1/1869 (o qualunque sia il tuo valore datetime predefinito) , il problema qui è che diventa difficile da gestire. Potresti usarlo come mancanza di valore, tranne che cosa succede se voglio filtrare tutti i miei record per i quali non ho una data di morte? Potrei facilmente filtrare le persone che sono effettivamente morte in quella data e che potrebbero causare problemi di integrità dei dati.

Il fatto è a volte si Non ha bisogno di rappresentare niente e sicuro a volte un tipo di variabile funziona bene per questo, ma spesso i tipi di variabili devono essere in grado di rappresentare nulla.

Se non ho mele ho 0 mele, ma cosa succede se non so quante mele ho?

Certamente null è abusato e potenzialmente pericoloso, ma a volte è necessario. È solo il default in molti casi perché fino a quando non fornisco un valore la mancanza di un valore e qualcosa deve rappresentarlo. (Nullo)


37
Null serves a very valid purpose of representing a lack of value.Un tipo Optiono Maybeserve a questo scopo molto valido senza bypassare il sistema dei tipi.
Doval,

34
Nessuno sostiene che non ci dovrebbe essere un valore di mancanza di valore, stanno sostenendo che i valori che potrebbero mancare dovrebbero essere esplicitamente contrassegnati come tali, piuttosto che ogni valore potenzialmente mancante.

2
Immagino che RualStorge stesse parlando in relazione a SQL, perché ci sono campi che affermano che ogni colonna dovrebbe essere contrassegnata come NOT NULL. La mia domanda non era collegata a RDBMS però ...
mrpyo

5
+1 per la distinzione tra "nessun valore" e "valore sconosciuto"
David

2
Non avrebbe più senso separare lo stato di una persona? Vale a dire un Persontipo ha un statecampo di tipo State, che è un'unione discriminata di Alivee Dead(dateOfDeath : Date).
Jon-Hanson,

10

Non andrei fino al punto in cui "ce l'hanno altre lingue, anche noi dobbiamo averlo ..." come se fosse una specie di tenere il passo con i Jones. Una caratteristica chiave di ogni nuova lingua è la capacità di interagire con le librerie esistenti in altre lingue (leggi: C). Poiché C ha puntatori nulli, il livello di interoperabilità necessita necessariamente del concetto di null (o di qualche altro equivalente "non esiste" che esplode quando lo si utilizza).

Il progettista del linguaggio avrebbe potuto scegliere di utilizzare i Tipi di opzione e costringerti a gestire il percorso null ovunque che le cose possano essere null. E questo quasi certamente porterebbe a meno bug.

Ma (specialmente per Java e C # a causa dei tempi della loro introduzione e del loro pubblico di destinazione) l'uso di tipi di opzioni per questo livello di interoperabilità avrebbe probabilmente danneggiato se non silurato la loro adozione. O il tipo di opzione è passato fino in fondo, infastidendo i programmatori C ++ dalla metà alla fine degli anni '90 - o il livello di interoperabilità genererebbe eccezioni quando si incontrano null, infastidendo i programmatori C ++ dalla metà alla fine degli anni '90. ..


3
Il primo paragrafo non ha senso per me. Java non ha l'interoperabilità C nella forma che suggerisci (c'è JNI ma salta già attraverso una dozzina di cerchi per tutto ciò che riguarda i riferimenti; inoltre è usato raramente in pratica), lo stesso per altri linguaggi "moderni".

@delnan - scusa, ho più familiarità con C #, che ha questo tipo di interoperabilità. Ho piuttosto ipotizzato che molte delle librerie Java di base usino anche JNI in fondo.
Telastyn,

6
Sei un buon argomento per consentire null, ma puoi comunque consentire null senza incoraggiarlo . Scala ne è un buon esempio. Può interagire senza soluzione di continuità con le API Java che usano null, ma sei incoraggiato a racchiuderlo in un modo Optionper utilizzarlo all'interno di Scala, che è facile come val x = Option(possiblyNullReference). In pratica, non ci vuole molto tempo perché le persone vedano i benefici di un Option.
Karl Bielefeldt,

1
I tipi di opzione vanno di pari passo con la corrispondenza dei modelli (verificata staticamente), che purtroppo C # non ha. F # però, ed è meraviglioso.
Steven Evers,

1
@SteveEvers È possibile simularlo utilizzando una classe base astratta con costruttore privato, classi interne sigillate e un Matchmetodo che accetta i delegati come argomenti. Quindi passi espressioni lambda a Match(punti bonus per l'utilizzo di argomenti con nome) e Matchchiama quello giusto.
Doval,

7

Prima di tutto, penso che possiamo essere tutti d'accordo sul fatto che sia necessario un concetto di nullità. Ci sono alcune situazioni in cui dobbiamo rappresentare l' assenza di informazioni.

Consentire nullriferimenti (e puntatori) è solo un'implementazione di questo concetto, e forse il più popolare sebbene sia noto per avere problemi: C, Java, Python, Ruby, PHP, JavaScript, ... tutti usano uno simile null.

Perché ? Bene, qual è l'alternativa?

In linguaggi funzionali come Haskell hai il tipo Optiono Maybe; tuttavia quelli sono basati su:

  • tipi parametrici
  • tipi di dati algebrici

Ora, l'originale C, Java, Python, Ruby o PHP supportavano entrambe queste funzionalità? No. I generici imperfetti di Java sono recenti nella storia del linguaggio e in qualche modo dubito che anche gli altri li implementino affatto.

Ecco qua. nullè facile, i tipi di dati algebrici parametrici sono più difficili. La gente cercava l'alternativa più semplice.


+1 per "null è facile, i tipi di dati algebrici parametrici sono più difficili." Ma penso che non sia stato un problema di tipizzazione parametrica e di ADT più difficili; è solo che non sono percepiti come necessari. Se Java fosse stato spedito senza un sistema a oggetti, d'altra parte, sarebbe floppato; OOP era una funzionalità "da spettacolo", in quanto se non lo avevi, nessuno era interessato.
Doval,

@Doval: beh, OOP avrebbe potuto essere necessario per Java, ma non per C :) Ma è vero che Java mirava ad essere semplice. Sfortunatamente la gente sembra supporre che un linguaggio semplice porti a programmi semplici, il che è un po 'strano (Brainfuck è un linguaggio molto semplice ...), ma siamo certamente d'accordo sul fatto che i linguaggi complicati (C ++ ...) non sono una panacea, anche se possono essere incredibilmente utili.
Matthieu M.,

1
@MatthieuM .: I sistemi reali sono complessi. Un linguaggio ben progettato le cui complessità corrispondono al sistema del mondo reale da modellare può consentire al sistema complesso di essere modellato con un semplice codice. I tentativi di semplificare eccessivamente una lingua semplicemente spingono la complessità sul programmatore che la sta usando.
supercat

@supercat: non potrei essere più d'accordo. O come viene parafrasato Einstein: "Rendi tutto il più semplice possibile, ma non più semplice".
Matthieu M.,

@MatthieuM .: Einstein era saggio in molti modi. Le lingue che cercano di assumere "tutto è un oggetto, un riferimento al quale può essere memorizzato Object" non riconoscono che le applicazioni pratiche hanno bisogno di oggetti mutabili non condivisi e oggetti immutabili condivisibili (entrambi i quali dovrebbero comportarsi come valori), nonché condivisibili e non condivisibili entità. L'uso di un solo Objecttipo per tutto non elimina la necessità di tali distinzioni; rende semplicemente più difficile usarli correttamente.
supercat

5

Null / nil / none in sé non è male.

Se guardi il suo famoso discorso fuorviante "L'errore di miliardi di dollari", Tony Hoare parla di come consentire a qualsiasi variabile di essere in grado di contenere un valore nullo sia stato un errore enorme. L'alternativa - mediante le Opzioni - non senza di fatto sbarazzarsi di riferimenti nulli. Invece ti consente di specificare quali variabili possono contenere null e quali no.

È un dato di fatto, con i linguaggi moderni che implementano una corretta gestione delle eccezioni, gli errori null dereference non sono diversi da qualsiasi altra eccezione: lo trovi, lo risolvi. Alcune alternative ai riferimenti null (ad esempio il modello Null Object) nascondono errori, causando il fallimento silenzioso delle cose fino a molto tempo dopo. Secondo me, è molto meglio fallire velocemente .

Quindi la domanda è: perché le lingue non riescono a implementare le opzioni? È un dato di fatto, il linguaggio probabilmente più popolare di tutti i tempi C ++ ha la capacità di definire variabili oggetto che non possono essere assegnate NULL. Questa è una soluzione al "problema nullo" menzionato da Tony Hoare nel suo discorso. Perché il prossimo linguaggio di battitura più popolare, Java, non ce l'ha? Ci si potrebbe chiedere perché abbia così tanti difetti in generale, specialmente nel suo sistema di tipi. Non credo che si possa davvero dire che le lingue commettono sistematicamente questo errore. Alcuni lo fanno, altri no.


1
Uno dei maggiori punti di forza di Java dal punto di vista dell'implementazione, ma dei punti deboli dal punto di vista del linguaggio, è che esiste un solo tipo non primitivo: il Promiscuous Object Reference. Ciò semplifica enormemente l'autonomia, rendendo possibili alcune implementazioni JVM estremamente leggere. Tale progettazione, tuttavia, significa che ogni tipo deve avere un valore predefinito e, per un riferimento a oggetti promiscui, l'unico default possibile è null.
supercat,

Bene, un tipo di radice non primitiva in ogni caso. Perché questa è una debolezza dal punto di vista linguistico? Non capisco perché questo fatto richieda che ogni tipo abbia un valore predefinito (o viceversa perché più tipi di root consentirebbero ai tipi di non avere un valore predefinito), né perché questo sia un punto debole.
BT,

Quale altro tipo di non primitivo potrebbe contenere un campo o un elemento array? Il punto debole è che alcuni riferimenti vengono utilizzati per incapsulare l'identità e altri per incapsulare i valori contenuti all'interno degli oggetti identificati in tal modo. Per le variabili del tipo di riferimento utilizzate per incapsulare l'identità, nullè l'unico valore predefinito ragionevole. I riferimenti usati per incapsulare il valore, tuttavia, potrebbero avere un comportamento predefinito sensato nei casi in cui un tipo avrebbe o potrebbe costruire un'istanza predefinita ragionevole. Molti aspetti di come dovrebbero comportarsi i riferimenti dipendono da se e da come incapsulano il valore, ma ...
supercat

... il sistema di tipi Java non ha modo di esprimerlo. Se foocontiene l'unico riferimento a un int[]contenimento {1,2,3}e il codice vuole foocontenere un riferimento a un int[]contenimento {2,2,3}, il modo più rapido per ottenere ciò sarebbe incrementare foo[0]. Se il codice vuole far sapere a un metodo che è foovalido {1,2,3}, l'altro metodo non modificherà l'array né persisterà un riferimento oltre il punto in cui foovorrebbe modificarlo, il modo più rapido per ottenere ciò sarebbe passare un riferimento all'array. Se Java aveva un "riferimento di sola lettura effimero", allora ...
supercat

... l'array potrebbe essere passato in modo sicuro come riferimento effimero e un metodo che voleva mantenere il suo valore avrebbe saputo che doveva copiarlo. In assenza di un tale tipo, gli unici modi per esporre in modo sicuro il contenuto di un array sono o farne una copia o incapsularlo in un oggetto creato proprio a tale scopo.
supercat,

4

Perché i linguaggi di programmazione sono generalmente progettati per essere praticamente utili piuttosto che tecnicamente corretti. Il fatto è che gli nullstati sono un evento comune a causa di dati errati o mancanti o di uno stato che non è stato ancora deciso. Le soluzioni tecnicamente superiori sono tutte più ingombranti che semplicemente consentire stati nulli e risucchiare il fatto che i programmatori commettano errori.

Ad esempio, se voglio scrivere un semplice script che funziona con un file, posso scrivere pseudocodice come:

file = openfile("joebloggs.txt")

for line in file
{
  print(line)
}

e fallirà semplicemente se joebloggs.txt non esiste. Il fatto è che per semplici script che probabilmente vanno bene e per molte situazioni in codice più complesso so che esiste e il fallimento non accadrà, costringendomi a controllare i miei sprechi di tempo. Le alternative più sicure raggiungono la loro sicurezza costringendomi a gestire correttamente lo stato di potenziale fallimento ma spesso non voglio farlo, voglio solo andare avanti.


13
E qui hai dato un esempio di ciò che è esattamente sbagliato nei null. La funzione "openfile" correttamente implementata dovrebbe generare un'eccezione (per file mancante) che interrompe l'esecuzione proprio lì con la spiegazione esatta di ciò che è accaduto. Invece se restituisce null si propaga ulteriormente (a for line in file) e genera un'eccezione di riferimento null insignificante, il che è OK per un programma così semplice ma causa problemi di debug reali in sistemi molto più complessi. Se i null non esistessero, il progettista di "openfile" non sarebbe in grado di commettere questo errore.
mrpyo,

2
+1 per "Perché i linguaggi di programmazione sono generalmente progettati per essere praticamente utili anziché tecnicamente corretti"
Martin Ba,

2
Ogni tipo di opzione che conosco consente di eseguire il fail-on-null con una singola chiamata di metodo extra breve (esempio Rust:) let file = something(...).unwrap(). A seconda del POV, è un modo semplice per non gestire errori o affermazioni concise che non può verificarsi null. Il tempo perso è minimo e risparmi tempo in altri luoghi perché non devi capire se qualcosa può essere nullo. Un altro vantaggio (che di per sé può valere la chiamata extra) è che si ignora esplicitamente il caso di errore; quando fallisce ci sono pochi dubbi su cosa sia andato storto e su dove debba andare la correzione.

4
@mrpyo Non tutte le lingue supportano le eccezioni e / o la gestione delle eccezioni (a la try / catch). E si possono abusare anche delle eccezioni: "eccezione come controllo di flusso" è un modello comune. Questo scenario - un file non esiste - è AFAIK l'esempio citato più frequentemente di quell'anti-pattern. Sembrerebbe che stai sostituendo una cattiva pratica con un'altra.
David,

8
@mrpyo if file exists { open file }soffre di una condizione di gara. L'unico modo affidabile per sapere se l'apertura di un file avrà esito positivo è provare ad aprirlo.

4

Esistono usi chiari e pratici del puntatore NULL(o nil, o Nil, o null, o Nothingo come viene chiamato nella lingua preferita).

Per quelle lingue che non hanno un sistema di eccezione (ad es. C) un puntatore null può essere usato come segno di errore quando un puntatore deve essere restituito. Per esempio:

char *buf = malloc(20);
if (!buf)
{
    perror("memory allocation failed");
    exit(1);
}

Qui un NULLritorno da malloc(3)viene utilizzato come indicatore di errore.

Se utilizzato negli argomenti metodo / funzione, può indicare l'uso predefinito per l'argomento o ignorare l'argomento di output. Esempio sotto.

Anche per quelle lingue con meccanismo di eccezione, un puntatore nullo può essere utilizzato come indicazione di errore soft (ovvero, errori recuperabili) soprattutto quando la gestione delle eccezioni è costosa (ad esempio Objective-C):

NSError *err = nil;
NSString *content = [NSString stringWithContentsOfURL:sourceFile
                                         usedEncoding:NULL // This output is ignored
                                                error:&err];
if (!content) // If the object is null, we have a soft error to recover from
{
    fprintf(stderr, "error: %s\n", [[err localizedDescription] UTF8String]);
    if (!error) // Check if the parent method ignored the error argument
        *error = err;
    return nil; // Go back to parent layer, with another soft error.
}

Qui, l'errore soft non causa l'arresto anomalo del programma se non viene rilevato. Ciò elimina il folle tentativo di catch come Java e ha un miglior controllo nel flusso del programma poiché gli errori software non si interrompono (e le poche eccezioni rimanenti non sono in genere recuperabili e non vengono rilevate)


5
Il problema è che non c'è modo di distinguere le variabili che non dovrebbero mai contenere nullda quelle che dovrebbero. Ad esempio, se voglio un nuovo tipo che contiene 5 valori in Java, potrei usare un enum, ma quello che ottengo è un tipo che può contenere 6 valori (i 5 che volevo + null). È un difetto nel sistema dei tipi.
Doval,

@Doval Se questa è la situazione, basta assegnare un significato a NULL (o se si dispone di un valore predefinito, trattarlo come sinonimo del valore predefinito) o utilizzare il valore NULL (che non dovrebbe mai apparire in primo luogo) come indicatore di errore soft (cioè errore ma almeno non si arresta in modo anomalo)
Maxthon Chan

1
A @MaxtonChan Nullpuò essere assegnato un significato solo quando i valori di un tipo non portano dati (es. Valori enum). Non appena i valori sono più complicati (ad esempio una struttura), nullnon è possibile assegnare un significato che abbia senso per quel tipo. Non è possibile utilizzare a nullcome struttura o elenco. E, ancora una volta, il problema con l'utilizzo nullcome segnale di errore è che non possiamo dire cosa potrebbe restituire null o accettare null. Qualsiasi variabile nel tuo programma potrebbe essere a nullmeno che tu non sia estremamente meticoloso per controllare ogni singolo nullprima di ogni singolo utilizzo, cosa che nessuno fa.
Doval,

1
@Doval: non ci sarebbero particolari difficoltà intrinseche nell'avere un tipo di riferimento immutabile considerato nullcome un valore predefinito utilizzabile (ad esempio avere il valore predefinito di stringcomportarsi come una stringa vuota, come nel precedente modello a oggetti comuni). Tutto ciò che sarebbe stato necessario sarebbe stato l'uso delle lingue callpiuttosto che callvirtquando si invocavano membri non virtuali.
supercat,

@supercat Questo è un buon punto, ma ora non è necessario aggiungere il supporto per distinguere tra tipi immutabili e non immutabili? Non sono sicuro di quanto sia banale aggiungere una lingua.
Doval,

4

Esistono due problemi correlati, ma leggermente diversi:

  1. Dovrebbe nullesistere affatto? O dovresti sempre usare Maybe<T>dove null è utile?
  2. Tutti i riferimenti dovrebbero essere nullable? In caso contrario, quale dovrebbe essere l'impostazione predefinita?

    Dover dichiarare esplicitamente tipi di riferimento nullable come string?o simili eviterebbe la maggior parte (ma non tutte) delle nullcause dei problemi , senza essere troppo diversi da quelli a cui sono abituati i programmatori.

Sono almeno d'accordo con te sul fatto che non tutti i riferimenti dovrebbero essere nulli. Ma evitare null non è privo di complessità:

.NET inizializza tutti i campi default<T>prima che possano essere accessibili per la prima volta dal codice gestito. Ciò significa che per i tipi di riferimento è necessario nullo qualcosa di equivalente e che i tipi di valore possono essere inizializzati su un tipo di zero senza eseguire il codice. Sebbene entrambi questi defaultaspetti presentino gravi inconvenienti, la semplicità dell'inizializzazione potrebbe aver superato quelli negativi.

  • Ad esempio, puoi aggirare i campi richiedendo l'inizializzazione dei campi prima di esporre il thispuntatore al codice gestito. Spec # ha seguito questa strada, utilizzando una sintassi diversa dal concatenamento del costruttore rispetto a C #.

  • Per i campi statici assicurarsi che ciò sia più difficile, a meno che tu non imponga forti restrizioni sul tipo di codice che può essere eseguito in un inizializzatore di campo poiché non puoi semplicemente nascondere il thispuntatore.

  • Come inizializzare matrici di tipi di riferimento? Si consideri un List<T>che è supportato da un array con una capacità maggiore della lunghezza. Gli elementi rimanenti devono avere un certo valore.

Un altro problema è che non consente metodi come quelli bool TryGetValue<T>(key, out T value)che ritornano default(T)come valuese non trovassero nulla. Anche se in questo caso è facile sostenere che il parametro out sia in primo luogo un progetto errato e questo metodo dovrebbe restituire un'unione discriminante o forse invece.

Tutti questi problemi possono essere risolti, ma non è facile come "vietare null e tutto va bene".


L' List<T>IMHO è l'esempio migliore, perché richiederebbe che ognuno Tabbia un valore predefinito, che ogni articolo nel backing store sia un Maybe<T>campo "isValid" aggiuntivo, anche quando Tè un Maybe<U>, o che il codice per il List<T>comportamento si comporti in modo diverso se Tè di per sé un tipo nullable. Considererei l'inizializzazione degli T[]elementi come un valore predefinito come il minimo male di quelle scelte, ma ovviamente significa che gli elementi devono avere un valore predefinito.
supercat,

La ruggine segue il punto 1 - nessun null. Ceylon segue il punto 2 - impostazione predefinita non nulla. I riferimenti che possono essere nulli vengono dichiarati esplicitamente con un tipo di unione che include un riferimento o null, ma null non può mai essere il valore di un riferimento semplice. Di conseguenza, il linguaggio è completamente sicuro e non esiste NullPointerException perché non è semanticamente possibile.
Jim Balter,

2

I linguaggi di programmazione più utili consentono di scrivere e leggere elementi di dati in sequenze arbitrarie, in modo che spesso non sia possibile determinare staticamente l'ordine in cui si verificheranno le letture e le scritture prima dell'esecuzione di un programma. Ci sono molti casi in cui il codice memorizzerà effettivamente i dati utili in ogni slot prima di leggerlo, ma dove dimostrarlo sarebbe difficile. Pertanto, sarà spesso necessario eseguire programmi in cui sarebbe almeno teoricamente possibile che il codice tenti di leggere qualcosa che non è stato ancora scritto con un valore utile. Indipendentemente dal fatto che il codice sia legale o meno, non esiste un modo generale per impedire al codice di effettuare il tentativo. L'unica domanda è cosa dovrebbe accadere quando ciò accade.

Lingue e sistemi diversi adottano approcci diversi.

  • Un approccio sarebbe quello di dire che qualsiasi tentativo di leggere qualcosa che non è stato scritto innescherà un errore immediato.

  • Un secondo approccio è richiedere al codice di fornire un valore in ogni posizione prima che sia possibile leggerlo, anche se non ci sarebbe modo per il valore memorizzato di essere semanticamente utile.

  • Un terzo approccio è semplicemente ignorare il problema e lasciare che tutto ciò che accada "naturalmente" accada e basta.

  • Un quarto approccio è quello di dire che ogni tipo deve avere un valore predefinito, e qualsiasi slot che non è stato scritto con nient'altro verrà impostato automaticamente su quel valore.

L'approccio n. 4 è molto più sicuro dell'approccio n. 3 ed è generalmente più economico degli approcci n. 1 e n. 2. Ciò lascia quindi la domanda su quale dovrebbe essere il valore predefinito per un tipo di riferimento. Per tipi di riferimento immutabili, in molti casi avrebbe senso definire un'istanza predefinita e dire che il valore predefinito per qualsiasi variabile di quel tipo dovrebbe essere un riferimento a quell'istanza. Per i tipi di riferimento mutabili, tuttavia, ciò non sarebbe molto utile. Se si tenta di utilizzare un tipo di riferimento modificabile prima che sia stato scritto, in genere non esiste alcun corso di azione sicuro se non quello di intercettare al punto del tentativo di utilizzo.

Semanticamente parlando, se si dispone di una matrice customersdi tipo Customer[20]e si tenta di Customer[4].GiveMoney(23)non archiviare nulla Customer[4], l'esecuzione dovrà essere intrappolata. Si potrebbe sostenere che un tentativo di lettura Customer[4]dovrebbe intercettare immediatamente, piuttosto che attendere fino a quando il codice tenta di farlo GiveMoney, ma ci sono abbastanza casi in cui è utile leggere uno slot, scoprire che non ha un valore e quindi utilizzarlo l'informazione, che fallire il tentativo di lettura stesso sarebbe spesso un grosso fastidio.

Alcune lingue consentono di specificare che determinate variabili non devono mai contenere null e qualsiasi tentativo di memorizzare un null deve attivare una trap immediata. Questa è una funzione utile. In generale, tuttavia, qualsiasi linguaggio che consenta ai programmatori di creare matrici di riferimenti dovrà consentire la possibilità di elementi di array nulli, oppure forzare l'inizializzazione di elementi di array su dati che non possono essere significativi.


Non sarebbe un Maybe/ Optiontipo di risolvere il problema con 2 #, dal momento che se non si dispone di un valore per il vostro riferimento ancora , ma avrà in futuro, si può semplicemente memorizzare Nothingin un Maybe <Ref type>?
Doval,

@Doval: No, non risolverebbe il problema, almeno non senza introdurre nuovamente riferimenti null. Un "niente" dovrebbe comportarsi come un membro del tipo? In tal caso, quale? O dovrebbe generare un'eccezione? In tal caso, come stai meglio del semplice utilizzo nullcorretto / ragionevole?
cHao,

@Doval: il tipo di supporto di a deve List<T>essere a T[]o a Maybe<T>? Che dire del tipo di supporto di a List<Maybe<T>>?
supercat,

@supercat Non sono sicuro di come un tipo di supporto Maybeabbia senso Listpoiché Maybeha un solo valore. Volevi dire Maybe<T>[]?
Doval,

@cHao Nothingpuò essere assegnato solo a valori di tipo Maybe, quindi non è proprio come assegnare null. Maybe<T>e Tsono due tipi distinti.
Doval,
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.