Migliore spiegazione per le lingue senza null


225

Ogni tanto, quando i programmatori si lamentano di errori / eccezioni nulli, qualcuno ci chiede cosa facciamo senza null.

Ho un'idea di base della freddezza dei tipi di opzione, ma non ho le conoscenze o le abilità linguistiche per esprimerla al meglio. Qual è una grande spiegazione di quanto segue scritto in un modo accessibile al programmatore medio a cui potremmo indirizzare quella persona?

  • L'indisponibilità di avere riferimenti / puntatori è nullable per impostazione predefinita
  • Come funzionano i tipi di opzioni, comprese le strategie per facilitare il controllo di casi nulli come
    • corrispondenza del modello e
    • comprensioni monadiche
  • Soluzione alternativa come il messaggio che mangia zero
  • (altri aspetti che mi sono perso)

11
Se aggiungi tag a questa domanda per la programmazione funzionale o F # avrai sicuramente delle risposte fantastiche.
Stephen Swensen,

Ho aggiunto un tag di programmazione funzionale poiché il tipo di opzione proveniva dal mondo ml. Preferirei non contrassegnarlo con F # (troppo specifico). A proposito qualcuno con poteri di tassonomia deve aggiungere un tag di tipo forse o di tipo opzione.
Roman A. Taycher,

4
ho poco bisogno di tag così specifici, sospetto. I tag servono principalmente a consentire alle persone di trovare domande pertinenti (ad esempio, "domande che conosco molto e che saranno in grado di rispondere", e "programmazione funzionale" è molto utile lì. Ma qualcosa come "null" o " opzione-tipo "sono molto meno utili. Poche persone probabilmente monitoreranno un tag" tipo-opzione "alla ricerca di domande a cui possano rispondere.;)
jalf

Non dimentichiamo che uno dei motivi principali per null è che i computer si sono evoluti fortemente legati alla teoria degli insiemi. Null è uno degli insiemi più importanti in tutta la teoria degli insiemi. Senza di essa interi algoritmi si rompono. Ad esempio, esegui un ordinamento di unione. Ciò comporta la rottura di un elenco a metà più volte. Cosa succede se l'elenco contiene 7 articoli? Prima lo dividi in 4 e 3. Quindi 2, 2, 2 e 1. Quindi 1, 1, 1, 1, 1, 1, 1 e .... null! Null ha uno scopo, solo uno che non vedi praticamente. Esiste di più per il regno teorico.
Stevendesu,

6
@steven_desu - Non sono d'accordo. Nelle lingue "nullable", puoi avere un riferimento a un elenco vuoto [] e anche un riferimento a elenco null. Questa domanda riguarda la confusione tra i due.
Stusmith,

Risposte:


433

Penso che il breve riassunto del perché nulla sia indesiderabile è che gli stati insignificanti non dovrebbero essere rappresentabili .

Supponiamo che stia modellando una porta. Può essere in uno dei tre stati: aperto, chiuso ma sbloccato, chiuso e bloccato. Ora potrei modellarlo sulla falsariga di

class Door
    private bool isShut
    private bool isLocked

ed è chiaro come mappare i miei tre stati in queste due variabili booleane. Ma questo foglie quarto, Stato indesiderato disponibili: isShut==false && isLocked==true. Poiché i tipi che ho selezionato come mia rappresentazione ammettono questo stato, devo dedicare uno sforzo mentale per garantire che la classe non entri mai in questo stato (forse codificando esplicitamente un invariante). Al contrario, se stessi usando una lingua con tipi di dati algebrici o enumerazioni verificate che mi permettono di definire

type DoorState =
    | Open | ShutAndUnlocked | ShutAndLocked

allora potrei definire

class Door
    private DoorState state

e non ci sono più preoccupazioni. Il sistema di tipi garantirà che ci siano solo tre stati possibili per cui class Doordeve trovarsi un'istanza . Questo è ciò che i sistemi di tipo sono bravi - escludendo esplicitamente un'intera classe di errori in fase di compilazione.

Il problema nullè che ogni tipo di riferimento ottiene questo stato extra nel suo spazio che in genere è indesiderato. Una stringvariabile può essere qualsiasi sequenza di caratteri, o potrebbe essere questo folle nullvalore extra che non si associa al mio dominio problematico. Un Triangleoggetto ha tre Points, che hanno essi stessi Xe Yvalori, ma sfortunatamente la Points o la Trianglestessa potrebbe essere questo pazzo valore nullo che non ha senso per il dominio grafico in cui sto lavorando. Ecc.

Quando si intende modellare un valore eventualmente inesistente, è necessario attivarlo esplicitamente. Se il modo in cui intendo modellare le persone è che ognuno Personha a FirstNamee a LastName, ma solo alcune persone hanno MiddleNames, allora vorrei dire qualcosa come

class Person
    private string FirstName
    private Option<string> MiddleName
    private string LastName

dove stringqui si presume che sia un tipo non annullabile. Quindi non ci sono invarianti difficili da stabilire e nessun inaspettato NullReferenceExceptionquando si cerca di calcolare la lunghezza del nome di qualcuno. Il sistema dei tipi garantisce che qualsiasi codice che si occupa dei MiddleNameconti possa essere Noneconsiderato possibile , mentre qualsiasi codice che si occupa di FirstNamepuò presumere che vi sia un valore.

Quindi, ad esempio, usando il tipo sopra, potremmo creare questa stupida funzione:

let TotalNumCharsInPersonsName(p:Person) =
    let middleLen = match p.MiddleName with
                    | None -> 0
                    | Some(s) -> s.Length
    p.FirstName.Length + middleLen + p.LastName.Length

senza preoccupazioni. Al contrario, in una lingua con riferimenti nullable per tipi come stringa, quindi assumendo

class Person
    private string FirstName
    private string MiddleName
    private string LastName

finisci per creare cose come

let TotalNumCharsInPersonsName(p:Person) =
    p.FirstName.Length + p.MiddleName.Length + p.LastName.Length

che esplode se l'oggetto Persona in arrivo non ha l'invariante di tutto ciò che è non nullo, oppure

let TotalNumCharsInPersonsName(p:Person) =
    (if p.FirstName=null then 0 else p.FirstName.Length)
    + (if p.MiddleName=null then 0 else p.MiddleName.Length)
    + (if p.LastName=null then 0 else p.LastName.Length)

o forse

let TotalNumCharsInPersonsName(p:Person) =
    p.FirstName.Length
    + (if p.MiddleName=null then 0 else p.MiddleName.Length)
    + p.LastName.Length

supponendo che si passicuri che il primo / ultimo ci siano, ma che il mezzo può essere nullo, o forse fai controlli che generano diversi tipi di eccezioni o chissà cosa. Tutte queste folli scelte di implementazione e cose su cui pensare sorgono perché c'è questo stupido valore rappresentabile che non vuoi o di cui non hai bisogno.

Null in genere aggiunge complessità inutile. La complessità è nemica di tutto il software e dovresti cercare di ridurre la complessità ogni volta che sia ragionevole.

(Nota bene che c'è anche più complessità anche in questi semplici esempi. Anche se a FirstNamenon può essere null, a stringpuò rappresentare ""(la stringa vuota), che probabilmente non è nemmeno un nome di persona che intendiamo modellare. Come tale, anche con non- stringhe nullable, potrebbe comunque essere il caso che "rappresentiamo valori insignificanti". Ancora una volta, potresti scegliere di combatterlo tramite invarianti e codice condizionale in fase di esecuzione, oppure usando il sistema dei tipi (ad esempio per avere un NonEmptyStringtipo). quest'ultimo è forse sconsigliato (i tipi "buoni" sono spesso "chiusi" su una serie di operazioni comuni, e ad esempio NonEmptyStringnon sono chiusi.SubString(0,0)), ma mostra più punti nello spazio di progettazione. Alla fine della giornata, in ogni dato sistema di tipi, c'è una certa complessità che sarà molto brava a sbarazzarsi di, e un'altra complessità che è intrinsecamente più difficile da eliminare. La chiave di questo argomento è che in quasi tutti i sistemi di tipi, il passaggio da "riferimenti annullabili di default" a "riferimenti non annullabili di default" è quasi sempre una semplice modifica che rende il sistema di tipi molto migliore nella lotta contro la complessità e escludendo alcuni tipi di errori e stati insignificanti. Quindi è abbastanza folle che così tante lingue continuino a ripetere questo errore ancora e ancora.)


31
Ri: nomi - Anzi. E forse ti interessa modellare una porta che è aperta ma con il catenaccio della serratura sporgente, impedendo alla porta di chiudersi. C'è molta complessità nel mondo. La chiave non è quella di aggiungere più complessità quando si implementa la mappatura tra "stati del mondo" e "stati del programma" nel software.
Brian,

59
Cosa, non hai mai chiuso le porte?
Giosuè,

58
Non capisco il motivo per cui la gente si preoccupa della semantica di un determinato dominio. Brian ha rappresentato i difetti con null in modo conciso e semplice, sì ha semplificato il dominio del problema nel suo esempio dicendo che tutti hanno nomi e cognomi. Alla domanda è stata data una "T", Brian - se mai sei a Boston ti devo una birra per tutte le pubblicazioni che fai qui!
Akaphenom,

67
@akaphenom: grazie, ma nota che non tutte le persone bevono birra (sono un non bevitore). Ma apprezzo che stai solo usando un modello semplificato del mondo per comunicare gratitudine, quindi non mi dilungherò di più sui presupposti imperfetti del tuo modello mondiale. : P (Tanta complessità nel mondo reale! :))
Brian,

4
Stranamente, ci sono 3 porte di stato in questo mondo! Sono utilizzati in alcuni hotel come porte del bagno. Un pulsante funge da chiave dall'interno, che blocca la porta dall'esterno. Viene automaticamente sbloccato non appena il chiavistello si sposta.
comonad,

65

La cosa bella dei tipi di opzioni non è che sono opzionali. È che tutti gli altri tipi non lo sono .

A volte , dobbiamo essere in grado di rappresentare una sorta di stato "null". A volte dobbiamo rappresentare un'opzione "senza valore" così come gli altri possibili valori che una variabile può assumere. Quindi un linguaggio che non lo consente non sarà paralizzato.

Ma spesso non ne abbiamo bisogno e consentire un tale stato "null" porta solo a ambiguità e confusione: ogni volta che accedo a una variabile del tipo di riferimento in .NET, devo considerare che potrebbe essere nullo .

Spesso, in realtà non sarà mai nullo, perché il programmatore struttura il codice in modo che non possa mai accadere. Ma il compilatore non può verificarlo, e ogni volta che lo vedi, devi chiederti "questo può essere nullo? Devo controllare null qui?"

Idealmente, nei molti casi in cui null non ha senso, non dovrebbe essere consentito .

È difficile da ottenere in .NET, dove quasi tutto può essere nullo. Devi fare affidamento sull'autore del codice che stai chiamando per essere disciplinato e coerente al 100% e aver chiaramente documentato ciò che può e non può essere nullo, oppure devi essere paranoico e controllare tutto .

Tuttavia, se i tipi non sono nulli per impostazione predefinita , non è necessario verificare se sono nulli. Sai che non possono mai essere nulli, perché il compilatore / controllo tipo lo impone per te.

E poi abbiamo solo bisogno di una porta sul retro per i rari casi in cui ci facciamo necessità di gestire uno stato nullo. Quindi è possibile utilizzare un tipo di "opzione". Quindi consentiamo null nei casi in cui abbiamo preso la decisione consapevole che dobbiamo essere in grado di rappresentare il caso "nessun valore", e in ogni altro caso, sappiamo che il valore non sarà mai nullo.

Come altri hanno già detto, in C # o Java per esempio, null può significare una delle due cose:

  1. la variabile non è inizializzata. Questo, idealmente, non dovrebbe mai accadere. Una variabile non dovrebbe esistere a meno che non sia inizializzata.
  2. la variabile contiene alcuni dati "opzionali": deve essere in grado di rappresentare il caso in cui non ci sono dati . Questo a volte è necessario. Forse stai cercando di trovare un oggetto in un elenco e non sai in anticipo se è presente o meno. Quindi dobbiamo essere in grado di rappresentare che "nessun oggetto è stato trovato".

Il secondo significato deve essere preservato, ma il primo dovrebbe essere eliminato del tutto. E anche il secondo significato non dovrebbe essere il valore predefinito. È qualcosa a cui possiamo optare se e quando ne abbiamo bisogno . Ma quando non abbiamo bisogno di qualcosa per essere facoltativo, vogliamo che il controllo del tipo garantisca che non sarà mai nullo.


E nel secondo significato, vogliamo che il compilatore ci avvisi (si fermi?) Se proviamo ad accedere a tali variabili senza prima verificare la nullità. Ecco un ottimo articolo sulla prossima funzionalità C # null / non-null (finalmente!) Blogs.msdn.microsoft.com/dotnet/2017/11/15/…
Ohad Schneider,

44

Tutte le risposte finora si concentrano sul perché nullè una cosa negativa e su come sia utile se una lingua può garantire che determinati valori non saranno mai nulli.

Continuano quindi a suggerire che sarebbe un'idea abbastanza chiara se si impone la non annullabilità per tutti i valori, cosa che si può fare se si aggiunge un concetto simile Optiono Maybeper rappresentare tipi che potrebbero non avere sempre un valore definito. Questo è l'approccio adottato da Haskell.

Sono tutte cose buone! Ma non preclude l'uso di tipi esplicitamente nulli / non nulli per ottenere lo stesso effetto. Perché, quindi, Option è ancora una buona cosa? Dopo tutto, Scala supporta valori Null (è ha a, in modo che possa lavorare con le librerie Java), ma sostiene Optionspure.

D. Quindi quali sono i vantaggi oltre a poter rimuovere completamente i null da una lingua?

A. Composizione

Se si effettua una traduzione ingenua da un codice a rischio zero

def fullNameLength(p:Person) = {
  val middleLen =
    if (null == p.middleName)
      p.middleName.length
    else
      0
  p.firstName.length + middleLen + p.lastName.length
}

al codice compatibile con le opzioni

def fullNameLength(p:Person) = {
  val middleLen = p.middleName match {
    case Some(x) => x.length
    case _ => 0
  }
  p.firstName.length + middleLen + p.lastName.length
}

non c'è molta differenza! Ma è anche un modo terribile di usare le Opzioni ... Questo approccio è molto più pulito:

def fullNameLength(p:Person) = {
  val middleLen = p.middleName map {_.length} getOrElse 0
  p.firstName.length + middleLen + p.lastName.length
}

O anche:

def fullNameLength(p:Person) =       
  p.firstName.length +
  p.middleName.map{length}.getOrElse(0) +
  p.lastName.length

Quando inizi a trattare con Elenco di opzioni, diventa ancora meglio. Immagina che l'Elenco peoplesia esso stesso facoltativo:

people flatMap(_ find (_.firstName == "joe")) map (fullNameLength)

Come funziona?

//convert an Option[List[Person]] to an Option[S]
//where the function f takes a List[Person] and returns an S
people map f

//find a person named "Joe" in a List[Person].
//returns Some[Person], or None if "Joe" isn't in the list
validPeopleList find (_.firstName == "joe")

//returns None if people is None
//Some(None) if people is valid but doesn't contain Joe
//Some[Some[Person]] if Joe is found
people map (_ find (_.firstName == "joe")) 

//flatten it to return None if people is None or Joe isn't found
//Some[Person] if Joe is found
people flatMap (_ find (_.firstName == "joe")) 

//return Some(length) if the list isn't None and Joe is found
//otherwise return None
people flatMap (_ find (_.firstName == "joe")) map (fullNameLength)

Il codice corrispondente con controlli null (o anche elvis?: Operatori) sarebbe dolorosamente lungo. Il vero trucco qui è l'operazione flatMap, che consente la comprensione nidificata di Opzioni e raccolte in un modo che i valori nullable non potranno mai raggiungere.


8
+1, questo è un buon punto da sottolineare. Un addendum: finito nella terra di Haskell, flatMapsarebbe chiamato (>>=), cioè, l'operatore "vincolante" per le monadi. Proprio così, gli Haskeller apprezzano flatMapcosì tanto le cose da ping che le inseriamo nel logo della nostra lingua.
CA McCann,

1
+1 Speriamo che un'espressione di Option<T>non sia mai, mai nulla. Sfortunatamente, Scala è uhh, ancora legato a Java :-) (D'altra parte, se Scala non suonasse bene con Java, chi lo userebbe? Oo)

Abbastanza facile da fare: 'Elenco (null) .headOption'. Si noti che ciò significa una cosa molto diversa da un valore di ritorno di "Nessuno"
Kevin Wright,

4
Ti ho dato la grazia poiché mi piace molto quello che hai detto sulla composizione, che altre persone non sembravano menzionare.
Roman A. Taycher,

Ottima risposta con ottimi esempi!
thSoft

38

Dal momento che alla gente sembra mancare: nullè ambiguo.

La data di nascita di Alice è null. Cosa significa?

La data di morte di Bob è null. Cosa significa?

Un'interpretazione "ragionevole" potrebbe essere che la data di nascita di Alice esiste ma è sconosciuta, mentre la data di morte di Bob non esiste (Bob è ancora vivo). Ma perché siamo arrivati ​​a risposte diverse?


Un altro problema: nullè un caso limite.

  • È null = null?
  • È nan = nan?
  • È inf = inf?
  • È +0 = -0?
  • È +0/0 = -0/0?

Le risposte di solito sono rispettivamente "sì", "no", "sì", "sì", "no", "sì". I "matematici" pazzi chiamano NaN "nullità" e dicono che si confronta uguale a se stesso. SQL considera i null come non uguali a nulla (quindi si comportano come NaNs). Ci si chiede cosa succede quando si tenta di memorizzare ± ∞, ± 0 e NaN nella stessa colonna del database (ci sono 2 53 NaN, la metà dei quali sono "negativi").

A peggiorare le cose, i database differiscono nel modo in cui trattano NULL e la maggior parte di essi non è coerente (vedere Gestione NULL in SQLite per una panoramica). È piuttosto orribile.


E ora per la storia obbligatoria:

Di recente ho progettato una tabella di database (sqlite3) con cinque colonne a NOT NULL, b, id_a, id_b NOT NULL, timestamp. Poiché si tratta di uno schema generico progettato per risolvere un problema generico per app abbastanza arbitrarie, esistono due vincoli di unicità:

UNIQUE(a, b, id_a)
UNIQUE(a, b, id_b)

id_aesiste solo per la compatibilità con un design di app esistente (in parte perché non ho trovato una soluzione migliore) e non viene utilizzato nella nuova app. A causa del modo NULL lavora in SQL, posso inserire (1, 2, NULL, 3, t)e (1, 2, NULL, 4, t)non violare il primo vincolo di unicità (perché (1, 2, NULL) != (1, 2, NULL)).

Questo funziona in modo specifico a causa del modo in cui NULL opera in un vincolo di unicità sulla maggior parte dei database (presumibilmente, quindi è più facile modellare le situazioni del "mondo reale", ad esempio due persone non possono avere lo stesso numero di previdenza sociale, ma non tutte le persone ne hanno uno).


FWIW, senza prima invocare un comportamento indefinito, i riferimenti C ++ non possono "puntare a" null e non è possibile costruire una classe con variabili membro di riferimento non inizializzate (se viene generata un'eccezione, la costruzione fallisce).

Sidenote: Occasionalmente potresti volere puntatori che si escludono a vicenda (cioè solo uno di essi può essere non NULL), ad esempio in un ipotetico iOS type DialogState = NotShown | ShowingActionSheet UIActionSheet | ShowingAlertView UIAlertView | Dismissed. Invece, sono costretto a fare cose del genere assert((bool)actionSheet + (bool)alertView == 1).


Tuttavia, i matematici reali non usano il concetto di "NaN".
Noldorin,

@Noldorin: Lo fanno, ma usano il termine "forma indeterminata".
IJ Kennedy,

@IJKennedy: Questo è un college diverso, che conosco abbastanza bene grazie. Alcuni "NaN" possono rappresentare una forma indeterminata, ma poiché l'FPA non fa ragionamenti simbolici, equipararlo a una forma indeterminata è abbastanza fuorviante!
Noldorin,

Cosa c'è che non va assert(actionSheet ^ alertView)? O la tua lingua non può essere bocciata da XOR?
cat

16

La indesiderabilità di avere riferimenti / puntatori è nullable per impostazione predefinita.

Non penso che questo sia il problema principale con i null, il problema principale con i null è che possono significare due cose:

  1. Il riferimento / puntatore non è inizializzato: il problema qui è lo stesso della mutabilità in generale. Per uno, rende più difficile analizzare il tuo codice.
  2. La variabile essendo null significa in realtà qualcosa: questo è il caso in cui i tipi di Opzione effettivamente formalizzano.

Le lingue che supportano i tipi di opzione in genere proibiscono o scoraggiano anche l'uso di variabili non inizializzate.

Come funzionano i tipi di opzioni, comprese le strategie per facilitare il controllo di casi nulli come la corrispondenza dei modelli.

Per essere efficaci, i tipi di opzione devono essere supportati direttamente nella lingua. Altrimenti ci vuole molto codice della piastra della caldaia per simularli. La corrispondenza dei modelli e la deduzione dei tipi sono due caratteristiche del linguaggio chiave che rendono facile usare i tipi di opzione. Per esempio:

In F #:

//first we create the option list, and then filter out all None Option types and 
//map all Some Option types to their values.  See how type-inference shines.
let optionList = [Some(1); Some(2); None; Some(3); None]
optionList |> List.choose id //evaluates to [1;2;3]

//here is a simple pattern-matching example
//which prints "1;2;None;3;None;".
//notice how value is extracted from op during the match
optionList 
|> List.iter (function Some(value) -> printf "%i;" value | None -> printf "None;")

Tuttavia, in un linguaggio come Java senza supporto diretto per i tipi di Opzione, avremmo qualcosa del tipo:

//here we perform the same filter/map operation as in the F# example.
List<Option<Integer>> optionList = Arrays.asList(new Some<Integer>(1),new Some<Integer>(2),new None<Integer>(),new Some<Integer>(3),new None<Integer>());
List<Integer> filteredList = new ArrayList<Integer>();
for(Option<Integer> op : list)
    if(op instanceof Some)
        filteredList.add(((Some<Integer>)op).getValue());

Soluzione alternativa come il messaggio che mangia zero

Il "messaggio che mangia zero" di Objective-C non è tanto una soluzione quanto un tentativo di alleggerire il mal di testa del controllo nullo. Fondamentalmente, invece di generare un'eccezione di runtime quando si tenta di invocare un metodo su un oggetto null, l'espressione restituisce invece null stessa. Sospendere l'incredulità, è come se ogni metodo di istanza inizi con if (this == null) return null;. Ma poi c'è una perdita di informazioni: non sai se il metodo ha restituito null perché è un valore di ritorno valido o perché l'oggetto è effettivamente nullo. È un po 'come la deglutizione delle eccezioni e non fa alcun progresso nell'affrontare i problemi con null delineato prima.


Questa è una pipì da compagnia ma c # non è quasi un linguaggio simile a c.
Roman A. Taycher,

4
Stavo andando per Java qui, dal momento che C # avrebbe probabilmente una soluzione migliore ... ma apprezzo la tua passione, quello che la gente intende davvero è "un linguaggio con sintassi ispirata al c". Sono andato avanti e ho sostituito la frase "c-like".
Stephen Swensen,

Con linq, giusto. Stavo pensando a c # e non me ne sono accorto.
Roman A. Taycher,

1
Sì, principalmente con la sintassi ispirata a c, ma penso di aver sentito parlare anche di linguaggi di programmazione imperativi come python / ruby ​​con pochissima sintassi di tipo c, definita come c-like dai programmatori funzionali.
Roman A. Taycher,

11

L'assembly ci ha portato indirizzi noti anche come puntatori non tipizzati. C li ha mappati direttamente come puntatori tipizzati ma ha introdotto il valore null di Algol come un valore di puntatore univoco, compatibile con tutti i puntatori digitati. Il grosso problema con null in C è che poiché ogni puntatore può essere nullo, non si può mai usare un puntatore in modo sicuro senza un controllo manuale.

Nei linguaggi di livello superiore, avere null è imbarazzante poiché trasmette davvero due nozioni distinte:

  • Dire che qualcosa non è definito .
  • Dire che qualcosa è facoltativo .

Avere variabili non definite è praticamente inutile e produce comportamenti indefiniti ogni volta che si verificano. Suppongo che tutti saranno d'accordo sul fatto che avere cose indefinite dovrebbe essere evitato a tutti i costi.

Il secondo caso è facoltativo ed è meglio fornito esplicitamente, ad esempio con un tipo di opzione .


Diciamo che siamo in una compagnia di trasporti e dobbiamo creare un'applicazione per aiutarci a creare un programma per i nostri autisti. Per ogni conducente, memorizziamo alcune informazioni quali: la patente di guida in suo possesso e il numero di telefono da chiamare in caso di emergenza.

In C avremmo potuto:

struct PhoneNumber { ... };
struct MotorbikeLicence { ... };
struct CarLicence { ... };
struct TruckLicence { ... };

struct Driver {
  char name[32]; /* Null terminated */
  struct PhoneNumber * emergency_phone_number;
  struct MotorbikeLicence * motorbike_licence;
  struct CarLicence * car_licence;
  struct TruckLicence * truck_licence;
};

Come noterai, in qualsiasi elaborazione sul nostro elenco di driver dovremo verificare la presenza di puntatori null. Il compilatore non ti aiuterà, la sicurezza del programma dipende dalle tue spalle.

In OCaml, lo stesso codice sarebbe simile al seguente:

type phone_number = { ... }
type motorbike_licence = { ... }
type car_licence = { ... }
type truck_licence = { ... }

type driver = {
  name: string;
  emergency_phone_number: phone_number option;
  motorbike_licence: motorbike_licence option;
  car_licence: car_licence option;
  truck_licence: truck_licence option;
}

Diciamo ora che vogliamo stampare i nomi di tutti i conducenti insieme ai loro numeri di licenza del camion.

In C:

#include <stdio.h>

void print_driver_with_truck_licence_number(struct Driver * driver) {
  /* Check may be redundant but better be safe than sorry */
  if (driver != NULL) {
    printf("driver %s has ", driver->name);
    if (driver->truck_licence != NULL) {
      printf("truck licence %04d-%04d-%08d\n",
        driver->truck_licence->area_code
        driver->truck_licence->year
        driver->truck_licence->num_in_year);
    } else {
      printf("no truck licence\n");
    }
  }
}

void print_drivers_with_truck_licence_numbers(struct Driver ** drivers, int nb) {
  if (drivers != NULL && nb >= 0) {
    int i;
    for (i = 0; i < nb; ++i) {
      struct Driver * driver = drivers[i];
      if (driver) {
        print_driver_with_truck_licence_number(driver);
      } else {
        /* Huh ? We got a null inside the array, meaning it probably got
           corrupt somehow, what do we do ? Ignore ? Assert ? */
      }
    }
  } else {
    /* Caller provided us with erroneous input, what do we do ?
       Ignore ? Assert ? */
  }
}

In OCaml questo sarebbe:

open Printf

(* Here we are guaranteed to have a driver instance *)
let print_driver_with_truck_licence_number driver =
  printf "driver %s has " driver.name;
  match driver.truck_licence with
    | None ->
        printf "no truck licence\n"
    | Some licence ->
        (* Here we are guaranteed to have a licence *)
        printf "truck licence %04d-%04d-%08d\n"
          licence.area_code
          licence.year
          licence.num_in_year

(* Here we are guaranteed to have a valid list of drivers *)
let print_drivers_with_truck_licence_numbers drivers =
  List.iter print_driver_with_truck_licence_number drivers

Come puoi vedere in questo banale esempio, nella versione sicura non c'è nulla di complicato:

  • È più difficile.
  • Ottieni garanzie molto migliori e non è richiesto alcun controllo nullo.
  • Il compilatore ha assicurato che hai gestito correttamente l'opzione

Considerando che in C, potresti aver dimenticato un controllo nullo e boom ...

Nota: questi esempi di codice non sono stati compilati, ma spero che tu abbia avuto le idee.


Non l'ho mai provato ma en.wikipedia.org/wiki/Cyclone_%28programming_language%29 afferma di consentire puntatori non nulli per c.
Roman A. Taycher,

1
Non sono d'accordo con la tua affermazione che nessuno è interessato al primo caso. Molte persone, specialmente quelle nelle comunità linguistiche funzionali, sono estremamente interessate a questo e scoraggiano o vietano completamente l'uso di variabili non inizializzate.
Stephen Swensen,

Credo che NULLcome "riferimento che potrebbe non indicare nulla" è stato inventato per alcune lingue algol (Wikipedia concorda, vedi en.wikipedia.org/wiki/Null_pointer#Null_pointer ). Ma ovviamente è probabile che i programmatori di assembly abbiano inizializzato i loro puntatori a un indirizzo non valido (leggi: Null = 0).

1
@Stephen: probabilmente intendevamo la stessa cosa. Per me scoraggiano o vietano l'uso di cose non inizializzate proprio perché non ha senso discutere cose indefinite poiché non possiamo fare nulla di sano o utile con esse. Non avrebbe alcun interesse.
bltxd,

2
come @tc. dice, null non ha nulla a che fare con l'assemblaggio. Nel complesso, i tipi sono generalmente non annullabile. Un valore caricato in un registro per uso generico potrebbe essere zero o potrebbe essere un numero intero diverso da zero. Ma non può mai essere nullo. Anche se si carica un indirizzo di memoria in un registro, sulle architetture più comuni, non esiste una rappresentazione separata del "puntatore nullo". Questo è un concetto introdotto in linguaggi di livello superiore, come C.
jalf

5

Microsoft Research ha un progetto interessante chiamato

Spec #

È un'estensione C # con tipo non nullo e un meccanismo per verificare che i tuoi oggetti non siano nulli , anche se IMHO, applicando il principio di progettazione secondo il contratto può essere più appropriato e più utile per molte situazioni problematiche causate da riferimenti null.


4

Provenendo da un background .NET, ho sempre pensato che null avesse un punto, è utile. Fino a quando non sono venuto a conoscenza di strutture e di quanto fosse facile lavorare con loro evitando un sacco di codice della caldaia. Tony Hoare, parlando al QCon London nel 2009, si è scusato per aver inventato il riferimento null . Per citarlo:

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. Negli ultimi anni, numerosi analizzatori di programmi come PREfix e PREfast in Microsoft sono stati utilizzati per controllare i riferimenti e fornire avvisi in caso di rischio che possano essere non nulli. Linguaggi di programmazione più recenti come Spec # hanno introdotto dichiarazioni per riferimenti non nulli. Questa è la soluzione, che ho respinto nel 1965.

Vedi anche questa domanda ai programmatori



1

Ho sempre considerato Null (o zero) come l'assenza di un valore .

A volte lo vuoi, a volte no. Dipende dal dominio con cui stai lavorando. Se l'assenza è significativa: nessun secondo nome, l'applicazione può agire di conseguenza. D'altra parte, se il valore null non dovesse essere presente: il nome è null, quindi lo sviluppatore riceve la proverbiale telefonata alle 2 del mattino.

Ho anche visto il codice sovraccarico e troppo complicato con controlli per null. Per me questo significa una delle due cose:
a) un bug più in alto nella struttura dell'applicazione
b) progettazione errata / incompleta

Sul lato positivo: Null è probabilmente una delle nozioni più utili per verificare se qualcosa è assente e le lingue senza il concetto di null finiranno per complicare eccessivamente le cose quando è il momento di fare la validazione dei dati. In questo caso, se una nuova variabile non è inizializzata, detti linguaggi imposteranno le variabili su una stringa vuota, 0 o una raccolta vuota. Tuttavia, se una stringa vuota o 0 o una raccolta vuota sono valori validi per la tua applicazione, allora hai un problema.

A volte ciò è stato eluso inventando valori speciali / strani per i campi per rappresentare uno stato non inizializzato. Ma poi cosa succede quando il valore speciale viene inserito da un utente ben intenzionato? E non entriamo nel caos che questo farà delle routine di convalida dei dati. Se la lingua supportasse il concetto nullo, tutte le preoccupazioni svanirebbero.


Ciao @Jon, è un po 'difficile seguirti qui. Ho finalmente capito che per valori "speciali / strani" probabilmente intendi qualcosa come "non definito" di Javascript o "NaN" di IEEE. Ma a parte questo, in realtà non affronti nessuna delle domande poste dall'OP. E l'affermazione che "Null è probabilmente la nozione più utile per verificare se qualcosa è assente" è quasi certamente sbagliata. I tipi di opzione sono un'alternativa ben considerata e sicura al tipo a null.
Stephen Swensen,

@Stephen - In realtà guardando indietro al mio messaggio, penso che l'intera seconda metà dovrebbe essere spostata su una domanda ancora da porre. Ma dico ancora null è molto utile per verificare se qualcosa è assente.
Jon,

0

Le lingue vettoriali a volte possono cavarsela senza avere un null.

Il vettore vuoto funge da null tipizzato in questo caso.


Penso di aver capito di cosa stai parlando ma potresti elencare alcuni esempi? Soprattutto di applicare più funzioni a un valore eventualmente nullo?
Roman A. Taycher,

Se si applica una trasformazione vettoriale a un vettore vuoto, si ottiene un altro vettore vuoto. Cordiali saluti, SQL è principalmente un linguaggio vettoriale.
Giosuè,

1
OK, lo chiarisco meglio. SQL è un linguaggio vettoriale per le righe e un linguaggio dei valori per le colonne.
Giosuè,
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.