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

78

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

Perché hanno deciso di includerlo comunque? Sono sicuro che la mancanza di riferimenti nulli incoraggerebbe (o addirittura forzerebbe) un codice di qualità migliore (in particolare una migliore progettazione delle librerie) sia dai creatori linguistici che dagli utenti.

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

    
posta mrpyo 02.05.2014 - 14:19
fonte

10 risposte

92

Dichiarazione di non responsabilità: poiché non conosco nessun progettista di lingua personalmente, qualsiasi risposta che darò sarà speculativa.

Da Tony Hoare stesso:

I call it my billion-dollar mistake. It was the invention of the null reference in 1965. At that time, I was designing the first comprehensive type system for references in an object oriented language (ALGOL W). My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn't resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.

Enfasi sulla mia.

Naturalmente non gli sembrava una cattiva idea in quel momento. È probabile che sia stato perpetuato in parte per lo stesso motivo: se fosse sembrata una buona idea per l'inventore di quicksort vincitore del Turing Award, non sorprende che molte persone ancora non capiscano perché è il male. È anche probabile in parte perché è conveniente che le nuove lingue siano simili alle lingue più vecchie, sia per ragioni di marketing e di curva di apprendimento. Caso in questione:

"We were after the C++ programmers. We managed to drag a lot of them about halfway to Lisp." -Guy Steele, co-author of the Java spec

(Fonte: link )

E, naturalmente, C ++ ha null perché C ha null, e non c'è bisogno di entrare nell'impatto storico di C. Il tipo C # di J ++ sostituito, che era l'implementazione di Microsoft di Java, ed è anche sostituito da C ++ come linguaggio di scelta per lo sviluppo di Windows, quindi potrebbe averlo ottenuto da uno di questi.

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

Programming languages on the whole are very much more complicated than they used to be: object orientation, inheritance, and other features are still not really being thought through from the point of view of a coherent and scientifically well-based discipline or a theory of correctness. My original postulate, which I have been pursuing as a scientist all my life, is that one uses the criteria of correctness as a means of converging on a decent programming language design—one which doesn’t set traps for its users, and ones in which the different components of the program correspond clearly to different components of its specification, so you can reason compositionally about it. [...] The tools, including the compiler, have to be based on some theory of what it means to write a correct program. -Oral history interview by Philip L. Frana, 17 July 2002, Cambridge, England; Charles Babbage Institute, University of Minnesota.[ http://www.cbi.umn.edu/oh/display.phtml?id=343]

Di nuovo, enfasi mia. Sun / Oracle e Microsoft sono aziende e la linea di fondo di qualsiasi azienda è il denaro. I benefici per loro di avere null potrebbero aver superato gli svantaggi, oppure avrebbero semplicemente avuto una scadenza troppo ravvicinata per considerare pienamente il problema. Come esempio di un errore linguistico diverso che probabilmente si è verificato a causa delle scadenze:

It's a shame that Cloneable is broken, but it happens. The original Java APIs were done very quickly under a tight deadline to meet a closing market window. The original Java team did an incredible job, but not all of the APIs are perfect. Cloneable is a weak spot, and I think people should be aware of its limitations. -Josh Bloch

(Fonte: link )

    
risposta data 02.05.2014 - 14:38
fonte
119

I'm sure designers of languages like Java or C# knew issues related to existence of null references

Certo.

Also implementing an option type isn't really much more complex than null references.

Mi permetto di dissentire! Le considerazioni di progettazione che sono andate nei tipi di valori nullable in C # 2 sono state complesse, controverse e difficili. Hanno preso i team di progettazione di entrambi i linguaggi e il runtime per molti mesi di dibattito, l'implementazione di prototipi e così via, e infatti la semantica del box nullable è stata cambiata molto molto vicino al C # 2.0 di spedizione, che è stato molto controverso. p>

Why did they decide to include it anyway?

Tutto il design è un processo di scelta tra molti obiettivi sottilmente e grossolanamente incompatibili; Posso solo dare un breve abbozzo di alcuni dei fattori che dovrebbero essere considerati:

  • L'ortogonalità delle funzionalità linguistiche è generalmente considerata una buona cosa. C # ha tipi di valori nullable, tipi di valori non annullabili e tipi di riferimento nullable. I tipi di riferimento non annullabili non esistono, il che rende il sistema di tipi non ortogonale.

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

  • L'interoperabilità semplice con COM è importante.

  • L'interoperabilità semplice con tutti gli altri linguaggi .NET è importante.

  • L'interoperabilità semplice con i database è importante.

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

  • Il costo delle prestazioni è importante.

  • Essere sensibili all'analisi statica è importante.

  • La coerenza del sistema di tipi è importante; possiamo sempre sapere che un riferimento non nullable è mai in qualsiasi circostanze considerate non valide? Cosa succede nel costruttore di un oggetto con un campo di riferimento non nullable? Che cosa succede nel finalizzatore di un tale oggetto, dove l'oggetto è finalizzato perché il codice che doveva riempire il riferimento ha generato un'eccezione ? Un sistema di tipi che ti sta a cuore sulle sue garanzie è pericoloso.

  • E per quanto riguarda la coerenza della semantica? I valori nulli si propagano quando usati, ma i riferimenti nulli generano eccezioni quando vengono utilizzati. Questo è incoerente; è questa incoerenza giustificata da qualche beneficio?

  • Possiamo implementare la funzione senza interrompere altre funzionalità? Quali altre possibili caratteristiche future la funzione impedisce?

  • Vai in guerra con l'esercito che hai, non quello che desideri. Ricorda, C # 1.0 non ha generici, quindi parlare di Maybe<T> come alternativa è un non-start completo. Se .NET è scivolato per due anni mentre il team di runtime ha aggiunto i generici, solo per eliminare i riferimenti null?

  • E riguardo la coerenza del sistema di tipi? Puoi dire Nullable<T> per qualsiasi tipo di valore - no, aspetta, è una bugia. Non puoi dire Nullable<Nullable<T>> . Dovresti essere in grado di? Se sì, quali sono le semantiche desiderate? Vale la pena di fare in modo che l'intero sistema di tipi abbia un caso speciale solo per questa funzione?

E così via. Queste decisioni sono complesse.

    
risposta data 02.05.2014 - 23:13
fonte
27

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

Dirò che sono la persona più vocale che conosco sugli abusi di null e tutti i mal di testa e le sofferenze che possono causare specialmente se usati liberamente.

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

Esempio che giustifica i null:

La data della morte è in genere un campo nullable. Ci sono tre possibili situazioni con la 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 viene visualizzata quando si crea un nuovo datetime che varia in base alla lingua utilizzata, ma tecnicamente c'è la possibilità che la persona in effetti muoia in quel momento e che venga contrassegnata come "valore vuoto" se si usa la data predefinita.

I dati dovrebbero rappresentare correttamente la situazione.

La persona è morta è nota la data della morte (3/9/1984)

Semplice, '3/9/1984'

La persona è morta, data di morte sconosciuta

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 della morte non è applicabile

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

Quindi lascia pensare ogni valore oltre ...

  • Null , ha implicazioni e preoccupazioni di cui devi essere cauto, tentando accidentalmente di manipolarlo senza confermare che non è null, per esempio genererebbe un'eccezione, ma rappresenta anche meglio la situazione attuale ... Se la persona non è morta, la data di morte non esiste ... non è nulla ... è nulla ...
  • 0/0/0000 , potrebbe essere accettabile in alcune lingue e potrebbe persino essere una rappresentazione appropriata di nessuna data. Sfortunatamente alcuni linguaggi e convalide rifiuteranno questo come un datetime non valido che lo rende un no go in molti casi.
  • 1/1/1869 (o qualunque sia il valore di data / ora predefinito) , il problema qui è che diventa difficile da gestire. Potresti usarlo come mancanza di valore, eccetto cosa succede se voglio filtrare tutti i miei documenti per i quali non ho una data di morte? Potrei facilmente filtrare le persone che sono effettivamente decedute in quella data e che potrebbero causare problemi di integrità dei dati.

Il fatto è che a volte Do non deve rappresentare nulla e sicuramente a volte un tipo di variabile funziona bene, ma spesso i tipi di variabile devono essere in grado di rappresentare nulla.

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

Assolutamente nulla è abusato e potenzialmente pericoloso, ma a volte è necessario. In molti casi è solo l'impostazione predefinita perché fino a quando non fornisco un valore, la mancanza di un valore e qualcosa deve rappresentarlo. (Null)

    
risposta data 02.05.2014 - 21:42
fonte
9

Non andrei tanto lontano quanto "altre lingue ce l'hanno, dobbiamo averlo anche noi ..." come se fosse una specie di stare al passo con i Jones. Una caratteristica chiave di ogni nuova lingua è la capacità di interoperare con le librerie esistenti in altre lingue (leggi: C). Dato che C ha dei puntatori nulli, lo strato di interoperabilità ha necessariamente bisogno del concetto di null (o qualche altro equivalente "non esiste" che esplode quando lo si usa).

Il designer del linguaggio avrebbe potuto scegliere di utilizzare Tipi di opzioni e forzarti a gestire il percorso null ovunque che le cose potrebbero essere nulle. E questo quasi certamente porterebbe a meno bug.

Ma (specialmente per Java e C # a causa della tempistica 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 ++ della metà o della fine degli anni '90 - o il livello di interoperabilità genererebbe eccezioni quando si incontrano valori nulli, annoiando a malapena i programmatori C ++ della metà e della fine degli anni '90. ..

    
risposta data 02.05.2014 - 14:33
fonte
7

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

Consentire riferimenti a null (e puntatori) è solo un'implementazione di questo concetto, e probabilmente la più popolare anche se è nota per avere problemi: C, Java, Python, Ruby, PHP, JavaScript, ... tutti gli usi un null simile.

Perché? Bene, qual è l'alternativa?

Nei linguaggi funzionali come Haskell hai il Option o Maybe type; tuttavia quelli sono costruiti su:

  • tipi parametrici
  • tipi di dati algebrici

Ora, l'originale C, Java, Python, Ruby o PHP supportano entrambe queste funzionalità? No. I generici imperfetti di Java sono recenti nella storia della lingua e dubito che gli altri li possano persino implementare.

Ce l'hai. null è facile, i tipi di dati algebrici parametrici sono più difficili. Le persone hanno optato per l'alternativa più semplice.

    
risposta data 03.05.2014 - 17:19
fonte
4

Perché i linguaggi di programmazione sono generalmente progettati per essere praticamente utili piuttosto che tecnicamente corretti. Il fatto è che gli stati di null sono un'occorrenza 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 del semplice consentire stati nulli e risucchiare il fatto che i programmatori commettano errori.

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

file = openfile("joebloggs.txt")

for line in file
{
  print(line)
}

e fallirà semplicemente se joebloggs.txt non esiste. Il fatto è che, per gli script semplici che probabilmente sono a posto e per molte situazioni in codice più complesso, so che esiste e l'errore non si verificherà e quindi mi costringerà a controllare i miei sprechi. Le alternative più sicure raggiungono la loro sicurezza costringendomi a gestire correttamente il potenziale stato di errore, ma spesso non voglio farlo, voglio solo andare avanti.

    
risposta data 02.05.2014 - 19:27
fonte
4

Ci sono usi chiari e pratici del puntatore NULL (o nil , o Nil , o null , o Nothing o qualunque sia chiamato nella tua lingua preferita).

Per quei linguaggi che non hanno un sistema di eccezione (ad es. C) un puntatore nullo può essere usato come segno di errore quando deve essere restituito un puntatore. Ad esempio:

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

Qui un NULL restituito da malloc(3) è usato come marker di fallimento.

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

Anche per quei linguaggi con meccanismo di eccezione, un puntatore nullo può essere usato come indicazione di errore soft (ovvero, gli errori che sono recuperabili) specialmente 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 software non causa il crash del programma se non viene rilevato. Questo elimina il pazzo try-catch come Java e ha un migliore controllo nel flusso del programma, in quanto gli errori software non vengono interrotti (e le poche eccezioni hardware rimanenti di solito non sono ripristinabili e rimangono non rilevate)

    
risposta data 02.05.2014 - 19:43
fonte
4

Ci sono due problemi correlati, ma leggermente diversi:

  1. Dovrebbe esistere null ? 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?

    Dovendo dichiarare esplicitamente i tipi di riferimento nullable come string? o simili, si eviterebbe la maggior parte (ma non tutti) dei problemi causati da null , senza essere troppo diversi da quelli a cui i programmatori sono abituati.

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

.NET inizializza tutti i campi a default<T> prima di poterli accedere per primo dal codice gestito. Ciò significa che per i tipi di riferimento è necessario null o qualcosa di equivalente e che i tipi di valori possono essere inizializzati con qualche tipo di zero senza codice in esecuzione. Anche se entrambi hanno degli svantaggi gravi, la semplicità dell'inizializzazione di default può aver superato quella di svantaggio.

  • Per i campi di istanza puoi aggirare il problema richiedendo l'inizializzazione dei campi prima di esporre il puntatore this al codice gestito. La specifica # è andata su questa rotta, utilizzando una sintassi diversa dalla concatenazione del costruttore rispetto a C #.

  • Per campi statici assicurando che questo sia più difficile, a meno che non si pongano forti restrizioni sul tipo di codice che può essere eseguito in un inizializzatore di campo poiché non si può semplicemente nascondere il puntatore this .

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

Un altro problema è che non consente metodi come bool TryGetValue<T>(key, out T value) che restituiscono default(T) come value se non trovano nulla. Anche se in questo caso è facile sostenere che il parametro out è in primo luogo un cattivo design e che questo metodo dovrebbe restituire un unione discriminante o forse .

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

    
risposta data 02.05.2014 - 20:39
fonte
4

Null / nil / none stesso non è malvagio.

Se osservi il suo famoso fuoricampo "The Billion dollar Mistake", Tony Hoare parla di come permettere a qualsiasi di essere in grado di contenere null è stato un enorme errore. L'alternativa - usando Options - fa not di fatto eliminare i riferimenti null. Invece ti permette di specificare quali variabili possono contenere null e quali no.

Di fatto, con linguaggi moderni che implementano un'appropriata gestione delle eccezioni, gli errori di dereferenza null non sono diversi da qualsiasi altra eccezione: lo trovi, lo aggiusti. Alcune alternative ai riferimenti null (ad esempio il modello Null Object) nascondono errori, causando il fallimento silenzioso delle cose fino a molto più tardi. A mio parere, è molto meglio fallire velocemente .

Quindi la domanda è allora, perché le lingue non riescono ad implementare le opzioni? Di fatto, il linguaggio probabilmente più popolare di tutti i tempi C ++ ha la capacità di definire variabili oggetto che non possono essere assegnate a NULL . Questa è una soluzione al "problema nullo" di cui parlava Tony Hoare nel suo discorso. Perché il prossimo linguaggio tipizzato più popolare, Java, non ce l'ha? Si potrebbe chiedere perché ha così tanti difetti in generale, specialmente nel suo sistema di tipi. Non penso che tu possa davvero dire che le lingue sistematicamente fanno questo errore. Alcuni lo fanno, altri no.

    
risposta data 04.05.2014 - 05:30
fonte
2

La maggior parte dei linguaggi di programmazione utili consente agli elementi di dati di essere scritti e letti in sequenze arbitrarie, in modo che spesso non sia possibile determinare staticamente l'ordine in cui si verificano letture e scritture prima dell'esecuzione di un programma. Ci sono molti casi in cui il codice memorizza dati utili in ogni slot prima di leggerlo, ma dove è difficile dimostrarlo. 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 succedere quando ciò accade.

Lingue e sistemi diversi adottano approcci diversi.

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

  • Un secondo approccio consiste nel richiedere che il codice fornisca 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 accade "naturalmente" avvenga.

  • Un quarto approccio è quello di dire che ogni tipo deve avere un valore predefinito, e qualsiasi slot che non è stato scritto con nessun altro avrà come valore predefinito.

L'approccio n. 4 è molto più sicuro dell'approccio # 3, ed è in generale 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 i tipi di riferimento immutabili, in molti casi sarebbe opportuno definire un'istanza predefinita e affermare che l'impostazione predefinita per qualsiasi variabile di quel tipo dovrebbe essere un riferimento a tale istanza. Per i tipi di riferimento mutabili, tuttavia, ciò non sarebbe molto utile. Se viene fatto un tentativo di usare un tipo di riferimento mutabile prima che sia stato scritto, in genere non esiste una linea di condotta sicura ad eccezione del trap nel punto di tentativo di utilizzo.

Semanticamente parlando, se uno ha un array customers di tipo Customer[20] , e uno tenta Customer[4].GiveMoney(23) senza aver memorizzato nulla su Customer[4] , l'esecuzione deve essere intrappolata. Si potrebbe obiettare che un tentativo di leggere Customer[4] dovrebbe intercettare immediatamente, piuttosto che aspettare che il codice tenti di GiveMoney , ma ci sono abbastanza casi in cui è utile leggere uno slot, scoprire che non contiene un valore, e quindi fare uso di tali informazioni, che avere il tentativo di lettura stesso fallire sarebbe spesso un grave fastidio.

Alcune lingue consentono di specificare che alcune variabili non devono mai contenere null, e qualsiasi tentativo di memorizzare un valore null dovrebbe innescare una trappola immediata. Questa è una caratteristica 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 in dati che non possono essere significativi.

    
risposta data 03.05.2014 - 05:22
fonte

Leggi altre domande sui tag