Domanda

Abbiamo notato che molti bug nel nostro software sviluppato in C # (o Java) causano una NullReferenceException.

C'è un motivo per cui " null " è stato anche incluso nella lingua?

Dopo tutto, se non ci fosse " null " ;, non avrei nessun bug, giusto?

In altre parole, quale caratteristica della lingua non potrebbe funzionare senza null?

È stato utile?

Soluzione

Anders Hejlsberg, "padre C #", ha appena parlato di questo punto in la sua intervista a Computerworld :

  

Ad esempio, nel sistema dei tipi non abbiamo separazione tra valore e tipi di riferimento e nullabilità dei tipi. Questo può sembrare un po 'traballante o un po' tecnico, ma in C # i tipi di riferimento possono essere nulli, come le stringhe, ma i tipi di valore non possono essere nulli. Sarebbe sicuramente bello avere tipi di riferimento non annullabili, quindi puoi dichiarare che & # 8216; questa stringa non può mai essere nulla e voglio che il tuo compilatore controlli che non posso mai colpire un puntatore null qui & # 8217 ;.

     

Il 50% dei bug che le persone incontrano oggi, che codificano con C # nella nostra piattaforma, e lo stesso vale per Java del resto, sono probabilmente eccezioni di riferimento null. Se avessimo avuto un sistema di tipo più forte che ti consentirebbe di dire che questo parametro potrebbe non essere mai nullo, e il tuo compilatore ti preghiamo di verificarlo ad ogni chiamata, facendo un'analisi statica del codice & # 8217 ;. Quindi avremmo potuto eliminare le classi di bug.

Cyrus Najmabadi, un ex ingegnere progettista software del team C # (che ora lavora in Google), discute su tale argomento sul suo blog: ( 1st , 2nd , 3rd , 4th ). Sembra che il più grande ostacolo all'adozione di tipi non nullable sia che la notazione disturba i programmatori & # 8217; abitudini e base di codice. Qualcosa come il 70% dei riferimenti dei programmi C # rischia di finire come non annullabili.

Se vuoi davvero avere un tipo di riferimento non annullabile in C #, dovresti provare a usare Spec # che è un'estensione C # che consente l'uso di "! " come segno non annullabile.

static string AcceptNotNullObject(object! s)
{
    return s.ToString();
}

Altri suggerimenti

La nullità è una conseguenza naturale dei tipi di riferimento. Se hai un riferimento, deve fare riferimento a qualche oggetto o essere nullo. Se dovessi vietare la nullità, dovresti sempre assicurarti che ogni variabile sia stata inizializzata con un'espressione non nulla - e anche in questo caso avresti problemi se le variabili fossero lette durante la fase di inizializzazione.

Come proporresti di rimuovere il concetto di nullità?

Come molte altre cose nella programmazione orientata agli oggetti, tutto risale ad ALGOL. Tony Hoare just lo ha definito il suo "errore di miliardi di dollari." Se non altro, è un eufemismo.

Ecco una tesi davvero interessante su come realizzare nullability non è l'impostazione predefinita in Java. I parallelismi con C # sono evidenti.

Null in C # è principalmente un riporto da C ++, che aveva puntatori che non puntavano a nulla in memoria (o meglio, indirizzo 0x00 ). In questo intervista , Anders Hejlsberg afferma che avrebbe voluto aggiungere tipi di riferimento non annullabili in C #.

Null ha anche un posto legittimo in un sistema di tipi, tuttavia, come qualcosa di simile al tipo di fondo (dove oggetto è il tipo in alto ). In lisp, il tipo in basso è NIL e in Scala è Nothing .

Sarebbe stato possibile progettare C # senza null, ma poi dovresti trovare una soluzione accettabile per gli usi che le persone di solito hanno per null , come valore unitario , non trovato , valore predefinito , valore indefinito e None < T > . Probabilmente ci sarebbe stata meno adozione tra i programmatori C ++ e Java se ci fossero riusciti comunque. Almeno fino a quando non hanno visto che i programmi C # non hanno mai avuto eccezioni puntatore null.

La rimozione di null non risolve molto. Dovresti avere un riferimento predefinito per la maggior parte delle variabili impostato su init. Invece di eccezioni con riferimento null, si otterrebbero comportamenti imprevisti perché la variabile punta a oggetti errati. Almeno i riferimenti null falliscono rapidamente invece di causare comportamenti imprevisti.

Puoi guardare il modello a oggetti nulli per trovare un modo per risolvere parte di questo problema

Null è una funzione estremamente potente. Cosa fai se hai un'assenza di valore? È NULL!

Una scuola di pensiero è di non tornare mai nulla, un'altra è sempre. Ad esempio, alcuni dicono che dovresti restituire un oggetto valido ma vuoto.

Preferisco null per me, è un'indicazione più vera di ciò che è in realtà. Se non riesco a recuperare un'entità dal mio livello di persistenza, voglio null. Non voglio un valore vuoto. Ma sono io.

È particolarmente utile con i primitivi. Ad esempio, se ho true o false, ma viene utilizzato in un modulo di sicurezza, in cui un'autorizzazione può essere Consenti, Nega o non impostata. Bene, voglio che non sia impostato su null. Quindi posso usare Bool?

Ci sono molte altre cose su cui potrei andare avanti, ma la lascerò lì.

  

Dopotutto, se non ci fosse " null " ;, non avrei nessun bug, giusto?

La risposta è NO . Il problema non è che C # consente null, il problema è che hai dei bug che si manifestano con NullReferenceException. Come è già stato affermato, i null hanno uno scopo nella lingua per indicare un "vuoto" tipo di riferimento o un non valore (vuoto / niente / sconosciuto).

Null non causa NullPointerExceptions ...

I programmatori causano NullPointerExceptions.

Senza valori nulli torniamo a utilizzare un valore arbitrario effettivo per determinare che il valore restituito di una funzione o metodo non era valido. Devi ancora controllare il -1 restituito (o qualsiasi altra cosa), rimuovere i null non risolverà magicamente la pigrizia, ma la offuscherà timidamente.

La domanda può essere interpretata come " È meglio avere un valore predefinito per ogni tipo di riferimento (come String.Empty) o null? " ;. In questa prospettiva preferirei avere null, perché;

  • Non vorrei scrivere un valore predefinito costruttore per ogni classe che scrivo.
  • Non vorrei alcuni inutili memoria da allocare per tale valori predefiniti.
  • Verifica se a il riferimento è nullo è piuttosto più economico dei confronti di valore.
  • È altamente possibile avere più bug che lo sono più difficile da rilevare, invece di NullReferanceExceptions. È un buona cosa avere una tale eccezione che indica chiaramente che lo sono facendo (assumendo) qualcosa di sbagliato.

" Null " è incluso nella lingua perché abbiamo tipi di valore e tipi di riferimento. Probabilmente è un effetto collaterale, ma penso che sia buono. Ci dà molto potere su come gestiamo la memoria in modo efficace.

Perché abbiamo null? ...

I tipi di valore sono memorizzati nello "stack", il loro valore si trova direttamente in quel pezzo di memoria (cioè int x = 5 significa che la posizione di memoria per quella variabile contiene "5").

I tipi di riferimento invece hanno un " pointer " nello stack che punta al valore effettivo sull'heap (ovvero stringa x = " ello " significa che il blocco di memoria nello stack contiene solo un indirizzo che punta al valore effettivo sull'heap).

Un valore nullo significa semplicemente che il nostro valore nello stack non punta a nessun valore effettivo nell'heap: è un puntatore vuoto.

Spero di averlo spiegato abbastanza bene.

Se ricevi una 'NullReferenceException', forse continui a fare riferimento a oggetti che non esistono più. Questo non è un problema con "null", è un problema con il tuo codice che punta a indirizzi inesistenti.

Ci sono situazioni in cui null è un buon modo per indicare che un riferimento non è stato inizializzato. Questo è importante in alcuni scenari.

Ad esempio:

MyResource resource;
try
{
  resource = new MyResource();
  //
  // Do some work
  //
}
finally
{
  if (resource != null)
    resource.Close();
}

Ciò avviene nella maggior parte dei casi mediante l'uso di un'istruzione using . Ma il modello è ancora ampiamente utilizzato.

Per quanto riguarda NullReferenceException, la causa di tali errori è spesso facile da ridurre implementando uno standard di codifica in cui tutti i parametri sono stati verificati per la validità. A seconda della natura del progetto, trovo che nella maggior parte dei casi sia sufficiente controllare i parametri sui membri esposti. Se i parametri non rientrano nell'intervallo previsto, viene generata una ArgumentException o viene restituito un risultato di errore, a seconda del modello di gestione degli errori in uso.

Il controllo dei parametri non rimuove di per sé i bug, ma eventuali bug che si verificano sono più facili da individuare e correggere durante la fase di test.

Come nota, Anders Hejlsberg ha menzionato la mancanza di un'applicazione non nulla dei più grandi errori nella specifica C # 1.0 e che includerla ora è "difficile".

Se continui a pensare che un valore di riferimento non nullo applicato staticamente sia di grande importanza, puoi consultare spec # lingua. È un'estensione di C # in cui i riferimenti non nulli fanno parte del linguaggio. Ciò garantisce che un riferimento contrassegnato come non null non possa mai essere assegnato a un riferimento null.

Una risposta ha indicato che ci sono valori null nei database. È vero, ma sono molto diversi dai null in C #.

In C #, i null sono marcatori per un riferimento che non fa riferimento a nulla.

Nei database, i valori null sono marcatori per celle di valori che non contengono un valore. Per celle di valore, intendo generalmente l'intersezione di una riga e una colonna in una tabella, ma il concetto di celle di valore potrebbe essere esteso oltre le tabelle.

La differenza tra i due sembra banale, a prima vista. Ma non lo è.

Null poiché è disponibile in C # / C ++ / Java / Ruby è meglio visto come una stranezza di un oscuro passato (Algol) che in qualche modo è sopravvissuto fino ad oggi.

Lo usi in due modi:

  • Per dichiarare i riferimenti senza inizializzarli (non valido).
  • Per indicare opzionalità (OK).

Come hai indovinato, 1) è ciò che ci causa problemi senza fine nelle lingue imperative comuni e avrebbe dovuto essere bandito molto tempo fa, 2) è la vera caratteristica essenziale.

Esistono lingue che evitano 1) senza impedire 2).

Ad esempio OCaml è una lingua simile.

Una semplice funzione che restituisce un numero intero sempre crescente a partire da 1:

let counter = ref 0;;
let next_counter_value () = (counter := !counter + 1; !counter);;

E riguardo all'opzionalità:

type distributed_computation_result = NotYetAvailable | Result of float;;
let print_result r = match r with
    | Result(f) -> Printf.printf "result is %f\n" f
    | NotYetAvailable -> Printf.printf "result not yet available\n";;

Non posso parlare del tuo problema specifico, ma sembra che il problema non sia l'esistenza di null. Nulla esiste nei database, è necessario un modo per tenerne conto a livello di applicazione. Non credo sia l'unica ragione per cui esiste in .net, intendiamoci. Ma immagino sia uno dei motivi.

Sono sorpreso che nessuno abbia parlato di database per la loro risposta. I database hanno campi nullable e qualsiasi lingua che riceverà i dati da un DB deve gestirli. Ciò significa avere un valore nullo.

In effetti, questo è così importante che per tipi base come int puoi renderli nullable!

Considera anche i valori di ritorno dalle funzioni, cosa succede se si desidera che una funzione divida un paio di numeri e il denominatore potrebbe essere 0? L'unica "corretta" la risposta in tal caso sarebbe nulla. (So ??che, in un esempio così semplice, un'eccezione sarebbe probabilmente un'opzione migliore ... ma ci possono essere situazioni in cui tutti i valori sono corretti ma i dati validi possono produrre una risposta non valida o non calcolabile. Non sono sicuro che un'eccezione debba essere utilizzata in tali casi ...)

Oltre a TUTTI i motivi già menzionati, NULL è necessario quando è necessario un segnaposto per un oggetto non ancora creato. Per esempio. se hai un riferimento circolare tra una coppia di oggetti, allora hai bisogno di null poiché non puoi istanziare entrambi contemporaneamente.

class A {
  B fieldb;
}

class B {
  A fielda;
}

A a = new A() // a.fieldb is null
B b = new B() { fielda = a } // b.fielda isnt
a.fieldb = b // now it isnt null anymore

Modifica: potresti essere in grado di estrarre una lingua che funziona senza valori nulli, ma sicuramente non sarà una lingua orientata agli oggetti. Ad esempio, prolog non ha valori nulli.

Se crei un oggetto con una variabile di istanza che fa riferimento a un oggetto, quale valore suggeriresti abbia questa variabile prima di assegnare un riferimento ad esso?

Propongo:

  1. Ban Null
  2. Estendi booleani: True, False e FileNotFound

Comunemente - NullReferenceException significa che ad alcuni metodi non è piaciuto ciò che è stato consegnato e ha restituito un riferimento null, che è stato successivamente utilizzato senza controllare il riferimento prima dell'uso.

Questo metodo avrebbe potuto presentare qualche eccezione più dettagliata invece di restituire null, che è conforme alla modalità fail fast .

Oppure il metodo potrebbe restituirti null per comodità, in modo da poter scrivere if invece di provare ed evitare il " overhead " di un'eccezione.

  • Annullamento esplicito, anche se raramente è necessario. Forse si può vederlo come una forma di programmazione difensiva.
  • Usalo (o struttura nullable (T) per i non nullable) come flag per indicare un campo mancante quando si mappano i campi da un'origine dati (flusso di byte, tabella del database) a un oggetto. Può diventare molto ingombrante creare un flag booleano per ogni possibile campo nullable e potrebbe essere impossibile usare valori sentinella come -1 o 0 quando tutti i valori nell'intervallo di quel campo sono validi. Questo è particolarmente utile quando ci sono molti molti campi.

Che si tratti di casi d'uso o di abuso è soggettivo, ma a volte li utilizzo.

Mi spiace di aver risposto con quattro anni di ritardo, sono sorpreso che nessuna delle risposte finora abbia risposto alla domanda originale in questo modo:

Lingue come C # e Java , come C e altre lingue prima di loro, hanno null in modo che il programmatore possa scrivere velocemente , codice ottimizzato utilizzando i puntatori in modo efficiente.


  • Vista di basso livello

Prima un po 'di storia. Il motivo per cui è stato inventato null è per efficienza. Quando si esegue una programmazione di basso livello in assembly, non c'è astrazione, si hanno valori nei registri e si desidera sfruttarli al meglio. Definire zero come valore non un puntatore valido è un'eccellente strategia per rappresentare o un oggetto o niente .

Perché sprecare la maggior parte dei possibili valori di una parola di memoria perfettamente valida, quando si può avere un overhead di memoria zero, un'implementazione molto rapida del modello valore opzionale ? Questo è il motivo per cui null è così utile.

  • Vista di alto livello.

Semanticamente, null non è in alcun modo necessario per i linguaggi di programmazione. Ad esempio, nei linguaggi funzionali classici come Haskell o nella famiglia ML, non esiste un valore nullo, ma piuttosto tipi denominati Forse o Opzione. Rappresentano il concetto di livello più alto di valore opzionale senza preoccuparsi in alcun modo dell'aspetto del codice assembly generato (che sarà il lavoro del compilatore).

E anche questo è molto utile, perché consente al compilatore di catturare più bug , e questo significa meno NullReferenceExceptions.

  • Riunendoli

Contrariamente a questi linguaggi di programmazione di altissimo livello, C # e Java consentono un possibile valore null per ogni tipo di riferimento (che è un altro nome per il tipo che finirà per essere implementato usando i puntatori ).

Potrebbe sembrare una cosa negativa, ma la cosa positiva è che il programmatore può usare la conoscenza di come funziona sotto il cofano, per creare un codice più efficiente (anche se il linguaggio ha Garbage Collection).

Questo è il motivo per cui null esiste ancora oggi nelle lingue: un compromesso tra la necessità di un concetto generale di valore opzionale e la sempre presente necessità di efficienza.

La funzione che non può funzionare senza null è in grado di rappresentare " l'assenza di un oggetto "

L'assenza di un oggetto è un concetto importante. Nella programmazione orientata agli oggetti, ne abbiamo bisogno per rappresentare un'associazione tra oggetti che è facoltativa: l'oggetto A può essere collegato a un oggetto B, oppure A potrebbe non avere un oggetto B. Senza null potremmo comunque emularlo: per esempio potremmo usare un elenco di oggetti per associare B ad A. Tale elenco potrebbe contenere un elemento (uno B) o essere vuoto. Questo è un po 'scomodo e non risolve davvero nulla. Il codice che presuppone l'esistenza di una B, come aobj.blist.first (). Method () esploderà in modo simile a un'eccezione di riferimento null: (se blist è vuoto, qual è il comportamento di blist.first () ?)

Parlando di elenchi, null consente di terminare un elenco collegato. Un ListNode può contenere un riferimento a un altro ListNode che può essere nullo. Lo stesso si può dire di altre strutture dinamiche come gli alberi. Null ti consente di avere un normale albero binario i cui nodi foglia sono contrassegnati da riferimenti figlio nulli.

Le liste e gli alberi possono essere costruiti senza null, ma devono essere circolari, altrimenti infiniti / pigri. Questo sarebbe probabilmente considerato come un vincolo inaccettabile dalla maggior parte dei programmatori, che preferirebbero avere scelte nella progettazione di strutture di dati.

I dolori associati ai riferimenti null, come i riferimenti null che si verificano accidentalmente a causa di bug e causano eccezioni, sono in parte una conseguenza del sistema di tipi statici, che introduce un valore null in ogni tipo: esiste una stringa null, un numero intero null, null Widget, ...

In un linguaggio tipizzato in modo dinamico, può esserci un singolo oggetto null, che ha il suo tipo. Il risultato è che hai tutti i vantaggi rappresentativi di null, oltre a una maggiore sicurezza. Ad esempio, se si scrive un metodo che accetta un parametro String, si garantisce che il parametro sarà un oggetto stringa e non null. Non esiste un riferimento null nella classe String: qualcosa che è noto per essere una stringa non può essere l'oggetto null. I riferimenti non hanno un tipo in un linguaggio dinamico. Un percorso di archiviazione come un membro di classe o un parametro di funzione contiene un valore che può essere un riferimento a un oggetto. Quell'oggetto ha tipo, non riferimento.

Quindi queste lingue forniscono un modello pulito, più o meno matematicamente puro di "null", e poi quelle statiche lo trasformano in un po 'come un mostro di Frankenstein.

Se un framework consente la creazione di un array di qualche tipo senza specificare cosa si dovrebbe fare con i nuovi elementi, quel tipo deve avere un valore predefinito. Per i tipi che implementano la semantica di riferimento mutabile (*), nel caso generale non esiste un valore predefinito sensato. Ritengo un punto debole del framework .NET che non vi sia modo di specificare che una chiamata di funzione non virtuale debba sopprimere qualsiasi controllo null. Ciò consentirebbe a tipi immutabili come String di comportarsi come tipi di valore, restituendo valori sensibili per proprietà come Lunghezza.

(*) Notare che in VB.NET e C #, la semantica di riferimento mutabile può essere implementata da tipi di classe o di struttura; un tipo di struttura implementerebbe la semantica di riferimento mutabile fungendo da proxy per un'istanza di wrapping di un oggetto classe a cui contiene un riferimento immutabile.

Sarebbe anche utile se si potesse specificare che una classe dovrebbe avere una semantica di tipo valore mutabile non annullabile (il che implica che - almeno - un'istanza di un campo di quel tipo creerebbe una nuova istanza di oggetto usando un costruttore predefinito e che la copia di un campo di quel tipo creerebbe una nuova istanza copiando quella precedente (gestendo in modo ricorsivo tutte le classi nidificate di tipo valore).

Non è chiaro, tuttavia, esattamente quanto supporto dovrebbe essere integrato nel framework per questo. Avere il framework stesso in grado di riconoscere le distinzioni tra tipi di valori mutabili, tipi di riferimento mutabili e tipi immutabili, consentirebbe alle classi che a loro volta contengono riferimenti a una combinazione di tipi mutabili e immutabili di classi esterne di evitare efficacemente di fare copie non necessarie di oggetti profondamente immutabili.

Null è un requisito essenziale per qualsiasi linguaggio OO. Qualsiasi variabile oggetto a cui non è stato assegnato un riferimento oggetto deve essere nulla.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top