Perché la maggior parte dei linguaggi imperativi / OO "ben noti" consente l'accesso non controllato a tipi che possono rappresentare un valore "nulla"?


29

Ho letto della (non) comodità di avere nullinvece (per esempio) Maybe. Dopo aver letto questo articolo , sono convinto che sarebbe molto meglio usareMaybe (o qualcosa di simile). Tuttavia, sono sorpreso di vedere che tutti i linguaggi di programmazione imperativi o orientati agli oggetti "ben noti" continuano a essere utilizzati null(il che consente l'accesso non controllato a tipi che possono rappresentare un valore "nulla") e che Maybeviene utilizzato principalmente nei linguaggi di programmazione funzionale.

Ad esempio, guarda il seguente codice C #:

void doSomething(string username)
{
    // Check that username is not null
    // Do something
}

Qualcosa ha un cattivo odore qui ... Perché dovremmo controllare se l'argomento è nullo? Non dovremmo supporre che ogni variabile contenga un riferimento a un oggetto? Come puoi vedere, il problema è che per definizione quasi tutte le variabili possono contenere un riferimento null. E se potessimo decidere quali variabili sono "annullabili" e quali no? Ciò ci risparmierebbe molto impegno durante il debug e la ricerca di una "NullReferenceException". Immagina che, per impostazione predefinita, nessun tipo possa contenere un riferimento null . Invece, dichiareresti esplicitamente che una variabile può contenere un riferimento null , solo se ne hai davvero bisogno. Questa è l'idea dietro forse. Se hai una funzione che in alcuni casi fallisce (ad esempio divisione per zero), potresti restituire aMaybe<int>, affermando esplicitamente che il risultato può essere un int, ma anche nulla! Questo è uno dei motivi per preferire Forse anziché null. Se sei interessato a più esempi, ti suggerisco di leggere questo articolo .

I fatti sono che, nonostante gli svantaggi di rendere la maggior parte dei tipi nulli per impostazione predefinita, la maggior parte dei linguaggi di programmazione OO lo fanno effettivamente. Ecco perché mi chiedo:

  • Che tipo di argomenti dovresti implementare nullnel tuo linguaggio di programmazione anziché Maybe? Ci sono ragioni o è solo "bagaglio storico"?

Assicurati di aver compreso la differenza tra null e Forse prima di rispondere a questa domanda.


3
Suggerisco di leggere di Tony Hoare , in particolare il suo errore da miliardi di dollari.
Oded,

2
Bene, questa era la sua ragione. E il risultato sfortunato è che è stato un errore copiato nella maggior parte delle lingue che sono seguite, fino ad oggi. Ci sono lingue in cui il nullsuo concetto non esiste (IIRC Haskell ne è un esempio).
Oded,

9
Il bagaglio storico non è qualcosa da sottovalutare. E ricorda che i sistemi operativi su cui si basano sono stati scritti in lingue che ci sono state nullper molto tempo. Non è facile lasciar perdere.
Oded,

3
Penso che dovresti fare luce sul concetto di Forse invece di pubblicare un link.
JeffO,

1
@ GlenH7 Le stringhe in C # sono valori di riferimento (possono essere nulli). So che int è un valore primitivo, ma è stato utile mostrare l'uso di forse.
Aochagavia,

Risposte:


15

Credo che sia principalmente il bagaglio storico.

Il linguaggio più importante e più antico con nullè C e C ++. Ma qui nullha senso. I puntatori sono ancora un concetto abbastanza numerico e di basso livello. E come ha detto qualcun altro, nella mentalità dei programmatori C e C ++, dover dire esplicitamente che il puntatore può essere nullnon ha senso.

Secondo in linea arriva Java. Considerando che gli sviluppatori Java stavano provando ad avvicinarsi al C ++, in modo da rendere più semplice la transizione dal C ++ a Java, probabilmente non volevano sbagliare con questo concetto di base del linguaggio. Inoltre, l'implementazione esplicita nullrichiederebbe molto più sforzo, poiché è necessario verificare se il riferimento non nullo è effettivamente impostato correttamente dopo l'inizializzazione.

Tutte le altre lingue sono uguali a Java. Di solito copiano il modo in cui lo fanno C ++ o Java, e considerando quanto sia fondamentale il concetto implicito nulldi tipi di riferimento, diventa davvero difficile progettare un linguaggio che usi esplicitamente null.


"Probabilmente non volevano scherzare con un tale concetto fondamentale del linguaggio" Hanno già rimosso completamente i puntatori, anche la rimozione nullnon sarebbe un grande cambiamento, credo.
svick,

2
@svick Per Java, i riferimenti sostituiscono i puntatori. E in molti casi, i puntatori in C ++ sono stati usati come fanno i riferimenti Java. Alcuni sostengono addirittura che Java abbia dei puntatori ( programmers.stackexchange.com/questions/207196/… )
Euforico

Ho contrassegnato questo come la risposta corretta. Vorrei votarlo ma non ho abbastanza reputazione.
Aochagavia,

1
Essere d'accordo. Si noti che in C ++ (a differenza di Java e C), l'essere nulli è l'eccezione. std::stringnon può essere null. int&non può essere null. Una int*lattina e C ++ consente l'accesso non controllato ad essa per due motivi: 1. perché C ha fatto e 2. perché dovresti capire cosa stai facendo quando usi i puntatori non elaborati in C ++.
Salterio

@MSalters: se un tipo non ha un valore predefinito copiabile a bit, la creazione di un array di quel tipo richiederà la chiamata a un costruttore per ogni suo elemento prima di consentire l'accesso all'array stesso. Ciò potrebbe richiedere un lavoro inutile (se alcuni o tutti gli elementi verranno sovrascritti prima di essere letti), potrebbe introdurre complicazioni se il costruttore di un elemento successivo fallisce dopo che è stato costruito un elemento precedente e potrebbe finire per non realizzare molto comunque (se un valore corretto per alcuni elementi dell'array non può essere determinato senza leggere altri).
supercat

15

In realtà, nullè un'ottima idea. Dato un puntatore, vogliamo designare che questo puntatore non fa riferimento a un valore valido. Quindi prendiamo una posizione di memoria, la dichiariamo non valida e ci atteniamo a quella convenzione (una convenzione talvolta imposta con segfault). Ora ogni volta che ho un puntatore posso verificare se contiene Nothing( ptr == null) o Some(value)( ptr != null, value = *ptr). Voglio che tu capisca che questo equivale a un Maybetipo.

I problemi con questo sono:

  1. In molte lingue il sistema dei tipi non aiuta qui a garantire un riferimento non nullo.

    Questo è un bagaglio storico, poiché molti linguaggi imperativi o OOP tradizionali hanno avuto progressi incrementali nei loro sistemi di tipo rispetto ai predecessori. Piccoli cambiamenti hanno il vantaggio che le nuove lingue sono più facili da imparare. C # è un linguaggio tradizionale che ha introdotto strumenti a livello di linguaggio per gestire meglio i null.

  2. I progettisti di API potrebbero restituire un nullerrore, ma non un riferimento alla cosa vera e propria in caso di successo. Spesso, la cosa (senza riferimento) viene restituita direttamente. Questo appiattimento di un livello di puntatore rende impossibile l'uso nullcome valore.

    Questa è solo pigrizia da parte del progettista e non può essere aiutata senza imporre una nidificazione corretta con un sistema di tipo adeguato. Alcune persone potrebbero anche provare a giustificare ciò con considerazioni sulle prestazioni o con l'esistenza di controlli opzionali (una raccolta potrebbe restituire nullo l'articolo stesso, ma anche fornire un containsmetodo).

  3. In Haskell c'è una vista chiara sul Maybetipo come monade. Ciò semplifica la composizione delle trasformazioni sul valore contenuto.

    D'altra parte, linguaggi di basso livello come C trattano a malapena le matrici come un tipo separato, quindi non sono sicuro di cosa ci aspettiamo. Nei linguaggi OOP con polimorfismo parametrizzato, un Maybetipo controllato in fase di esecuzione è piuttosto banale da implementare.


"C # sta allontanando i riferimenti null." Quali passaggi sono quelli? Non ho visto nulla del genere nelle modifiche alla lingua. O vuoi dire solo che le librerie comuni stanno usando nullmeno di quanto non facessero in passato?
svick,

@svick Frase negativa da parte mia. " C # è un linguaggio tradizionale che ha introdotto strumenti a livello di linguaggio per gestire meglio nulls " - Sto parlando dei Nullabletipi e ??dell'operatore predefinito. Non risolve il problema in questo momento in presenza di codice legacy, ma è un passo verso un futuro migliore.
amon,

Sono d'accordo con te. Vorrei votare la tua risposta ma non ho abbastanza reputazione :( Tuttavia, Nullable funziona solo per i tipi primitivi. Quindi è solo un piccolo passo.
Aochagavia,

1
@svick Nullable non ha nulla a che fare con questo. Stiamo parlando di tutti i tipi di riferimento che implicitamente consentono un valore nullo, invece di avere un programmatore che lo definisca esplicitamente. E Nullable può essere utilizzato solo su tipi di valore.
Euforico,

@Euforico Penso che il tuo commento sia stato inteso come una risposta ad Amon, non ho menzionato Nullable.
svick,

9

La mia comprensione è che nullera un costrutto necessario per estrarre i linguaggi di programmazione dall'assemblea. 1 I programmatori avevano bisogno della capacità di indicare che un valore di puntatore o registro era not a valid valuee nulldivenne il termine comune per quel significato.

Rafforzando il punto che nullè solo una convenzione per rappresentare un concetto, il valore reale per l' nullusato per poter / può variare in base al linguaggio di programmazione e alla piattaforma.

Se stavi progettando una nuova lingua e volessi evitare, nullma maybeinvece utilizzarla , incoraggerei un termine più descrittivo come not a valid valueo navvper indicare l'intento. Ma il nome di quel non-valore è un concetto separato dal fatto che dovresti permettere ai non-valori di esistere nella tua lingua.

Prima di poter decidere su uno di questi due punti, è necessario definire il significato del maybesignificato per il proprio sistema. Potresti scoprire che è solo una ridenominazione del nullsignificato di not a valid valueo potresti avere una semantica diversa per la tua lingua.

Allo stesso modo, la decisione se verificare l'accesso rispetto nullall'accesso o al riferimento è un'altra decisione di progettazione della tua lingua.

Per fornire un po 'di storia, Caveva un'ipotesi implicita che i programmatori comprendessero cosa stavano tentando di fare durante la manipolazione della memoria. Poiché era un'astrazione superiore all'assemblea e ai linguaggi imperativi che lo precedevano, mi sarei avventurato che il pensiero di proteggere il programmatore da un riferimento errato non gli era venuto in mente.

Ritengo che alcuni compilatori o i loro strumenti aggiuntivi possano fornire una misura per verificare l'accesso ai puntatori non valido. Quindi altri hanno notato questo potenziale problema e preso misure per proteggerlo.

L'opportunità o meno di permetterlo dipende da cosa vuoi che la tua lingua compia e da quale grado di responsabilità vuoi spingere agli utenti della tua lingua. Dipende anche dalla tua capacità di creare un compilatore per limitare quel tipo di comportamento.

Quindi, per rispondere alle tue domande:

  1. "Che tipo di argomenti ..." - Beh, dipende da cosa vuoi che faccia la lingua. Se si desidera simulare l'accesso bare metal, è possibile autorizzarlo.

  2. "è solo bagaglio storico?" Forse, forse no. nullcertamente aveva / ha significato per un certo numero di lingue e aiuta a guidare l'espressione di quelle lingue. Il precedente storico potrebbe aver influenzato le lingue più recenti e il loro permesso, nullma è un po 'troppo agitare le mani e dichiarare il concetto di bagaglio storico inutile.


1 Vedi questo articolo di Wikipedia, sebbene sia riconosciuto credito per Hoare per valori nulli e linguaggi orientati agli oggetti. Credo che le lingue imperative progredissero lungo un albero genealogico diverso da quello dell'Algol.


Il punto è che alla maggior parte delle variabili, per esempio, in C # o Java, può essere assegnato un riferimento null. Sembra che sarebbe molto meglio assegnare riferimenti null solo agli oggetti che indicano esplicitamente che "Forse" non esiste alcun riferimento. Quindi, la mia domanda riguarda il "concetto" nullo, e non la parola.
Aochagavia,

2
"I riferimenti a puntatori null possono essere visualizzati come errori durante la compilazione" Quali compilatori possono farlo?
svick,

Per essere completamente onesti, non si assegna mai un riferimento null a un oggetto ... il riferimento all'oggetto non esiste (il riferimento non indica nulla (0x000000000), che è per definizione null).
mgw854,

La citazione della specifica C99 parla del carattere null , non del puntatore null , sono due concetti molto diversi.
svick,

2
@ GlenH7 Non lo fa per me. Il codice viene object o = null; o.ToString();compilato bene per me, senza errori o avvisi in VS2012. ReSharper si lamenta di ciò, ma non è il compilatore.
svick,

7

Se guardi gli esempi nell'articolo che hai citato, il più delle volte usando Maybenon accorcia il codice. Non elimina la necessità di verificare Nothing. L'unica differenza è che ti ricorda di farlo tramite il sistema dei tipi.

Nota, dico "ricordare", non forzare. I programmatori sono pigri. Se un programmatore è convinto che un valore non possa essere Nothing, stanno per dereferenziare il Maybesenza controllarlo, proprio come fanno ora a dereferenziare un puntatore nullo. Il risultato finale è convertire un'eccezione puntatore null in un'eccezione "forse vuota dereferenziata".

Lo stesso principio della natura umana si applica in altre aree in cui i linguaggi di programmazione cercano di costringere i programmatori a fare qualcosa. Ad esempio, i progettisti di Java hanno cercato di forzare le persone a gestire la maggior parte delle eccezioni, il che ha provocato un sacco di piatti che ignorano silenziosamente o propagano ciecamente eccezioni.

Ciò che rende Maybepiacevole è quando molte decisioni vengono prese tramite pattern matching e polimorfismo anziché controlli espliciti. Ad esempio, è possibile creare funzioni separate processData(Some<T>)e processData(Nothing<T>), con le quali non è possibile farlo null. Sposta automaticamente la gestione degli errori in una funzione separata, il che è molto desiderabile nella programmazione funzionale in cui le funzioni vengono passate e valutate pigramente piuttosto che essere sempre chiamate in modo discendente. In OOP, il modo preferito per disaccoppiare il codice di gestione degli errori è con le eccezioni.


Pensi che sarebbe una caratteristica desiderabile in una nuova lingua OO?
aochagavia,

Puoi implementarlo da solo se vuoi ottenere i benefici del polimorfismo. L'unica cosa di cui hai bisogno del supporto linguistico è la non annullabilità. Mi piacerebbe vedere un modo per specificarlo da solo, simile a const, ma renderlo facoltativo. Alcuni tipi di codice di livello inferiore, come ad esempio elenchi collegati, sarebbero molto fastidiosi da implementare con oggetti non annullabili.
Karl Bielefeldt,

2
Tuttavia, non è necessario verificare con il tipo Forse. Il tipo Maybe è una monade, quindi dovrebbe avere le funzioni map :: Maybe a -> (a -> b) e bind :: Maybe a -> (a -> Maybe b)definite su di esso, in modo da poter continuare e thread ulteriori calcoli per la maggior parte con rifusione usando un'istruzione if else. E getValueOrDefault :: Maybe a -> (() -> a) -> ati consente di gestire il caso nullable. È molto più elegante del pattern matching Maybe asull'esplicito.
DetriusXii,

1

Maybeè un modo molto funzionale di pensare a un problema: esiste una cosa e può avere o meno un valore definito. In senso orientato agli oggetti, tuttavia, sostituiamo l'idea di una cosa (non importa se ha un valore o meno) con un oggetto. Chiaramente, un oggetto ha un valore. In caso contrario, diciamo che l'oggetto è null, ma ciò che realmente intendiamo è che non esiste alcun oggetto. Il riferimento che abbiamo all'oggetto non punta a nulla. La traduzione Maybein un concetto di OO non fa nulla di nuovo, anzi, crea semplicemente un codice più ingombrante. Devi ancora avere un riferimento null per il valore diMaybe<T>. Devi ancora fare controlli nulli (in effetti, devi fare molti più controlli null, ingombrando il tuo codice), anche se ora sono chiamati "forse controlli". Certo, scriverai un codice più robusto come sostiene l'autore, ma direi che è solo il caso perché hai reso il linguaggio molto più astratto e ottuso, richiedendo un livello di lavoro che nella maggior parte dei casi non è necessario. Farei volentieri una NullReferenceException di tanto in tanto piuttosto che occuparmi del codice spaghetti facendo un controllo Forse ogni volta che accedo a una nuova variabile.


2
Penso che porterebbe a meno controlli null, perché devi solo verificare se vedi forse <T> e non devi preoccuparti del resto dei tipi.
Aochagavia,

1
@svick Maybe<T>deve consentire nullcome valore, perché il valore potrebbe non esistere. Se ho Maybe<MyClass>, e non ha un valore, il campo valore deve contenere un riferimento null. Non c'è nient'altro che sia verificabile in modo sicuro.
mgw854,

1
@ mgw854 Certo che c'è. Nel linguaggio OO, Maybepotrebbe essere una classe astratta, con due classi che ereditano da essa: Some(che ha un campo per il valore) e None(che non ha quel campo). In questo modo, il valore non lo è mai null.
svick,

6
Con "non-nullability" di default, e forse <T> potresti essere certo che alcune variabili contengano sempre oggetti. Vale a dire, tutte le variabili che non sono Forse <T>
aochagavia

3
@ mgw854 Il punto di questa modifica sarebbe una maggiore potenza espressiva per gli sviluppatori. In questo momento, lo sviluppatore deve sempre presumere che il riferimento o il puntatore possano essere nulli e deve fare un controllo per assicurarsi che ci sia un valore utilizzabile. Implementando questa modifica, dai agli sviluppatori il potere di dire che ha davvero bisogno di un valore valido e di avere un controllo del compilatore per assicurarsi che sia stato passato un valore valido. Ma continuando a dare l'opzione per gli sviluppatori, di opt-in e non avere un valore trasferito.
Euforico,

1

Il concetto di nullpuò essere facilmente ricondotto a C, ma non è questo il problema.

La mia lingua preferita di tutti i giorni è C # e vorrei continuare nullcon una differenza. C # ha due tipi di tipi, valori e riferimenti . I valori non possono mai essere null, ma ci sono momenti in cui mi piacerebbe poter esprimere che nessun valore va perfettamente bene. Per fare questo C # usa i Nullabletipi quindi intsarebbe il valore e int?il valore nullable. Ecco come penso che dovrebbero funzionare anche i tipi di riferimento.

Vedi anche: Il riferimento null potrebbe non essere un errore :

I riferimenti null sono utili e talvolta indispensabili (considera quanti problemi puoi restituire o meno una stringa in C ++). L'errore in realtà non sta nell'esistenza dei puntatori null, ma nel modo in cui il sistema di tipi li gestisce. Sfortunatamente la maggior parte delle lingue (C ++, Java, C #) non le gestisce correttamente.


0

Penso che ciò sia dovuto al fatto che la programmazione funzionale è molto preoccupata per i tipi , in particolare i tipi che combinano altri tipi (tuple, funzioni come tipi di prima classe, monadi, ecc.) Rispetto alla programmazione orientata agli oggetti (o almeno inizialmente).

Le versioni moderne dei linguaggi di programmazione di cui sto parlando (C ++, C #, Java) sono tutte basate su linguaggi che non avevano alcuna forma di programmazione generica (C, C # 1.0, Java 1). Senza questo, puoi ancora creare una sorta di differenza tra oggetti nullable e non nullable nel linguaggio (come i riferimenti C ++, che non possono essere null, ma sono anche limitati), ma è molto meno naturale.


Penso che nel caso della programmazione funzionale, è il caso di FP che non ha riferimenti o tipi di puntatore. In FP tutto è un valore. E se hai un tipo di puntatore, diventa facile dire "puntatore a niente".
Euforico,

0

Penso che il motivo fondamentale sia che sono necessari relativamente pochi controlli null per rendere un programma "sicuro" contro la corruzione dei dati. Se un programma tenta di utilizzare il contenuto di un elemento dell'array o di un altro percorso di archiviazione che si suppone sia stato scritto con un riferimento valido ma non lo è stato, il risultato migliore è generare un'eccezione. Idealmente, l'eccezione indica esattamente dove si è verificato il problema, ma ciò che conta è che venga generata una sorta di eccezione prima che il riferimento null venga archiviato da qualche parte che potrebbe causare il danneggiamento dei dati. A meno che un metodo non memorizzi un oggetto senza prima tentare di usarlo in qualche modo, un tentativo di utilizzare un oggetto - di per sé - costituirà una sorta di "controllo nullo".

Se si vuole garantire che un riferimento null che appare dove non dovrebbe causare un'eccezione particolare diversa da quella NullReferenceException, sarà spesso necessario includere controlli null in tutto il luogo. D'altra parte, il semplice fatto che si verifichi una certa eccezione prima che un riferimento null possa causare "danni" oltre a quelli già eseguiti richiederà spesso un numero relativamente limitato di test - i test sarebbero generalmente richiesti solo nei casi in cui un oggetto memorizzasse un riferimento senza tentare di usarlo e il riferimento null ne sovrascriverebbe uno valido o causerebbe un altro codice per interpretare erroneamente altri aspetti dello stato del programma. Tali situazioni esistono, ma non sono poi così comuni; la maggior parte dei riferimenti null accidentali verrà catturata molto rapidamentese uno controlla per loro o no .


questo post è piuttosto difficile da leggere (wall of text). Ti dispiacerebbe modificarlo in una forma migliore?
moscerino del

1
@gnat: è meglio?
supercat

0

" Maybe," come scritto, è un costrutto di livello superiore rispetto a null. Usando più parole per definirlo, Forse è "un puntatore a una cosa o un puntatore a nulla, ma al compilatore non sono state ancora fornite informazioni sufficienti per determinare quale". Questo ti costringe a controllare esplicitamente ogni valore costantemente, a meno che tu non costruisca una specifica del compilatore che sia abbastanza intelligente da tenere il passo con il codice che scrivi.

Puoi realizzare un'implementazione di Forse con un linguaggio che ha facilmente dei null. C ++ ha uno sotto forma di boost::optional<T>. Rendere l'equivalente di null con Forse è molto difficile. In particolare, se ho un Maybe<Just<T>>, non posso assegnarlo a null (perché un tale concetto non esiste), mentre un T**in una lingua con null è molto facile da assegnare a null. Questo costringe a usare Maybe<Maybe<T>>, il che è totalmente valido, ma ti costringerà a fare molti più controlli per usare quell'oggetto.

Alcuni linguaggi funzionali usano Forse perché null richiede un comportamento indefinito o la gestione delle eccezioni, nessuno dei quali è un concetto semplice da mappare nelle sintassi del linguaggio funzionale. Forse ricopre il ruolo molto meglio in tali situazioni funzionali, ma nei linguaggi procedurali, null è il re. Non è una questione di giusto o sbagliato, solo una questione di ciò che rende più facile dire al computer di fare quello che vuoi che faccia.

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.