Informazioni sul supporto dei valori Null

Completato

Se si lavora come sviluppatore .NET, è probabile che ci si sia imbattuti in System.NullReferenceException. Questa eccezione si verifica in fase di esecuzione quando un null viene dereferenziato, ovvero quando una variabile viene valutata in fase di esecuzione, ma la variabile fa riferimento a null. Questa eccezione è di gran lunga l'eccezione che si verifica più di frequente all'interno dell'ecosistema .NET. Il creatore di null, Sir Tony Hoare, si riferisce a null come l'"errore da un miliardo di dollari".

Nell'esempio seguente la variabile FooBar viene assegnata a null e immediatamente dereferenziata, espondendo quindi il problema:

// Declare variable and assign it as null.
FooBar fooBar = null;

// Dereference variable by calling ToString.
// This will throw a NullReferenceException.
_ = fooBar.ToString();

// The FooBar type definition.
record FooBar(int Id, string Name);

Il problema diventa molto più difficile da individuare come sviluppatore quando le app aumentano di dimensioni e complessità. Individuare potenziali errori come questo è un processo per gli strumenti e il compilatore C# è a disposizione per agevolare il compito.

Definizione della sicurezza Null

Il termine sicurezza Null definisce un set di funzionalità specifiche per i tipi nullable che consente di ridurre il numero di occorrenze NullReferenceException possibili.

Considerando l'esempio FooBar precedente, si potrebbe evitare l'eccezioneNullReferenceException verificando se la variabile fooBar era null prima di dereferenziarla:

// Declare variable and assign it as null.
FooBar fooBar = null;

// Check for null
if (fooBar is not null)
{
    _ = fooBar.ToString();
}

// The FooBar type definition for example.
record FooBar(int Id, string Name);

Per facilitare l'identificazione di scenari come questo, il compilatore può dedurre la finalità del codice e applicare il comportamento desiderato. Tuttavia, ciò è possibile solo quando è abilitato un contesto nullable. Prima di discutere del contesto nullable, vengono illustrati i possibili tipi nullable.

Tipi nullable

Prima di C# 2.0, solo i tipi riferimento erano nullable. Tipi di valore come int o DateTime non possono essere null. Se questi tipi vengono inizializzati senza un valore, viene eseguito il fallback al relativo valore default. Nel caso di un oggetto int, si tratta di 0. Per un DateTime, è DateTime.MinValue.

I tipi di riferimento di cui è stata creata un'istanza senza valori iniziali funzionano in modo diverso. Il valore default per tutti i tipi di riferimento è null.

Si consideri il frammento di codice C# seguente:

string first;                  // first is null
string second = string.Empty   // second is not null, instead it's an empty string ""
int third;                     // third is 0 because int is a value type
DateTime date;                 // date is DateTime.MinValue

Nell'esempio precedente:

  • first è null perché il tipo riferimento string è stato dichiarato ma non è stata effettuata alcuna assegnazione.
  • second viene assegnato a string.Empty quando viene dichiarato. L'oggetto non ha mai avuto un'assegnazione null.
  • third è 0 nonostante non sia stato assegnato. Si tratta di un struct (tipo valore) e ha un valore default di 0.
  • date non è inizializzato, ma il relativo valore default è System.DateTime.MinValue.

A partire da C# 2.0, è possibile definire i tipi valore nullable usando Nullable<T> (o T? per abbreviato). In questo modo, i tipi valore possono essere nullable. Si consideri il frammento di codice C# seguente:

int? first;            // first is implicitly null (uninitialized)
int? second = null;    // second is explicitly null
int? third = default;  // third is null as the default value for Nullable<Int32> is null
int? fourth = new();    // fourth is 0, since new calls the nullable constructor

Nell'esempio precedente:

  • first è null perché il tipo valore nullable non è inizializzato.
  • second viene assegnato a null quando viene dichiarato.
  • third è null come il valore default per Nullable<int> è null.
  • fourth è 0 come l'espressione new() chiama il costruttore Nullable<int> e int è 0 per impostazione predefinita.

C# 8.0 ha introdotto tipi riferimento nullable, in cui è possibile esprimere la finalità che un tipo riferimento potrebbe essere null o è sempre non null. È possibile che si pensi "Credevo che tutti i tipi di riferimento ammettessero valori Null!". Non è sbagliato ed è proprio così. Questa funzionalità consente di esprimere la finalità, che il compilatore tenta quindi di applicare. La stessa sintassi T? esprime che un tipo riferimento deve essere nullable.

Si consideri il frammento di codice C# seguente:

#nullable enable

string first = string.Empty;
string second;
string? third;

Dato l'esempio precedente, il compilatore deduce la finalità come indicato di seguito:

  • first non è mai null poiché è sicuramente assegnato.
  • second non deve mai essere null, anche se inizialmente è null. Valutare second prima di assegnare un valore genera un avviso del compilatore perché non è inizializzato.
  • third potrebbe essere null. Ad esempio, potrebbe puntare a System.String, ma potrebbe puntare a null. Una di queste varianti è accettabile. Il compilatore consente di avvisare se si dereferenzia third senza prima verificare che non sia Null.

Importante

Per usare la funzionalità dei tipi riferimento nullable, come illustrato in precedenza, deve trovarsi all'interno di un contesto nullable. Questa operazione è descritta in dettaglio nella sezione successiva.

Contesto nullable

I contesti nullable consentono il controllo con granularità fine di come il compilatore interpreta le variabili dei tipi riferimento. Esistono quattro possibili contesti nullable:

  • disable: il compilatore si comporta in modo analogo a C# 7.3 e versioni precedenti.
  • enable: il compilatore abilita tutte le analisi dei riferimenti Null e tutte le funzionalità del linguaggio.
  • warnings: il compilatore esegue tutte le analisi Null e genera avvisi quando il codice potrebbe dereferenziare null.
  • annotations: il compilatore non esegue l'analisi Null o genera avvisi quando il codice potrebbe dereferenziare null, ma è comunque possibile annotare il codice usando tipi riferimento nullable ? e gli operatori di forgiving null (!).

Questo modulo ha come ambito i contesti nullable disable o enable. Per altre informazioni, vedere Tipi riferimento Nullable: contesti nullable.

Abilitare i tipi riferimento nullable

Nel file di progetto C# (con estensione csproj) aggiungere un nodo figlio <Nullable> all'elemento <Project> (o accodarlo a un oggetto esistente <PropertyGroup>). Verrà applicato il contesto nullable enable all'intero progetto.

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net6.0</TargetFramework>
        <Nullable>enable</Nullable>
    </PropertyGroup>

    <!-- Omitted for brevity -->

</Project>

In alternativa, è possibile definire l'ambito del contesto nullable in un file C# usando una direttiva del compilatore.

#nullable enable

La direttiva del compilatore C# precedente è funzionalmente equivalente alla configurazione del progetto, ma ha come ambito il file in cui si trova. Per altre informazioni, vedere Tipi riferimento Nullable: contesti nullable (documentazione)

Importante

Il contesto nullable è abilitato nel file con estensione csproj per impostazione predefinita in tutti i modelli di progetto C# a partire da .NET 6.0 e versioni successive.

Quando il contesto nullable è abilitato, verranno visualizzati nuovi avvisi. Si consideri l'esempio precedente FooBar, che prevede due avvisi quando viene analizzata in un contesto nullable:

  1. La riga FooBar fooBar = null; contiene un avviso per l'assegnazione null: Avviso C# CS8600: Conversione di valori letterali Null o valore null possibile in un tipo non nullable.

    Screenshot dell'avviso CS8600 di C#: Conversione del valore letterale Null o di un possibile valore Null in tipo non nullable.

  2. La riga _ = fooBar.ToString(); include anche un avviso. Questa volta il compilatore è preoccupato per il fatto che fooBar può essere null: Avviso C# CS8602: Dereferenziazione di un riferimento possibilmente null.

    Screenshot dell'avviso CS8602 di C#: Dereferenziamento di un possibile riferimento Null.

Importante

Non esiste alcuna sicurezza null garantita, anche se si reagisce e si eliminano elimina tutti gli avvisi. Esistono alcuni scenari limitati che supereranno l'analisi del compilatore, ma genereranno un runtime NullReferenceException.

Riepilogo

In questa unità si è appreso come abilitare un contesto nullable in C# per proteggersi da NullReferenceException. Nell'unità successiva verranno fornite altre informazioni su come esprimere esplicitamente la propria finalità all'interno di un contesto che ammette i valori Null.