Comprendere la nullabilità

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 compito per gli strumenti, e il compilatore C# è qui per fornire aiuto.

Definizione della sicurezza Null

Il termine Null safety definisce un set di funzionalità specifiche per i tipi annullabili 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 annullabile. Prima di discutere del contesto nullable, descriviamo i possibili tipi nullable.

Tipi nullable

Prima di C# 2.0, solo i tipi di riferimento erano nullabili. Tipi di valore come int o DateTimenon possono essere null. Se questi tipi vengono inizializzati senza un valore, tornano al loro 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 di 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. Si potrebbe pensare, "Ho pensato che tutti i tipi riferimento sono nullable!" Non è sbagliato ed è proprio così. Questa funzionalità consente di esprimere la finalità, che il compilatore tenta quindi di applicare. La stessa sintassi T? indica che un tipo di riferimento può essere annullabile.

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é è assegnaton in maniera definitiva.
  • secondnon deve mai essere null, anche se inizialmente è null. Valutare second prima di assegnare un valore genera un avviso del compilatore perché non è inizializzato.
  • thirdpotrebbe esserenull. Ad esempio, potrebbe puntare a System.String, ma potrebbe puntare a null. Una di queste varianti è accettabile. Il compilatore ti avvisa se dereferenzi third senza prima verificare che non sia null.

Importante

Per utilizzare la funzionalità dei tipi di riferimento nullable, come illustrato in precedenza, questa 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 di nullità né genera avvisi quando il codice potrebbe dereferenziare null, ma è comunque possibile annotare il codice usando tipi riferimento nullabile ? e gli operatori null-forgiving (!).

Questo modulo ha come ambito i contesti nullable disable o enable. Per altre informazioni, vedere Tipi riferimento Nullable: Contesto 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 ulteriori informazioni, vedere Tipi di riferimento annullabili: contesti annullabili (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 relativo all'assegnazione null: Avviso C# CS8600: Conversione del valore letterale Null o di un possibile valore Null in un tipo che non ammette i valori Null.

    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 imparerai di più su come esprimere esplicitamente il tuo intento in un contesto nullable.