Menu

Nullable in C#: int?, operatore ?? , ?. e controlli su null

Tutto su null in C#: tipi valore nullable (int?, Nullable<T>) con HasValue, Value e GetValueOrDefault, gli operatori di null coalescing ?? e ??=, l'operatore condizionale null ?., i controlli su null e i tipi riferimento nullable (string?) di C# 8.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

null significa "nessun valore". I tipi riferimento (classi, stringhe, array) hanno sempre potuto contenerlo. I tipi valore (int, decimal, bool, DateTime, struct) no, a meno che non li rendi nullable con ?. C# ti offre poi un piccolo insieme di operatori per lavorare con valori che potrebbero mancare: ??, ??= e ?..

Tipi valore nullable: int?

Aggiungi ? a un tipo valore per permettere null. int? è una scorciatoia per la struct Nullable<int>, che contiene un valore più un flag che dice se il valore è presente:

Output:

False
True
True
5.50
0
3
Rated 4 stars
4

Le fonti tipiche di tipi valore nullable sono i campi facoltativi dei moduli, i parametri di query e le colonne NULL di un database: un Order con un DateTime? ShippedAt non è stato spedito finché il valore è null. L'alternativa, un valore magico come -1 o DateTime.MinValue, sembra un dato reale e prima o poi viene usata come tale.

Un int? si converte in int solo in modo esplicito, perché la conversione può fallire. Questo ci porta all'unica eccezione che lanciano i tipi valore nullable.

"Nullable object must have a value"

Leggere .Value quando non c'è un valore, o convertire con un cast un int? null in int, lancia InvalidOperationException:

Output:

InvalidOperationException: Nullable object must have a value.
0
0
not stocked

stock is int count è un pattern: verifica se c'è un valore e lo estrae in count in un solo passo, il che si legge meglio di HasValue più Value. La pagina sul pattern matching ne mostra altre forme.

Aritmetica e confronti con null

Gli operatori sui tipi valore nullable sono lifted (sollevati): se uno dei due operandi è null, il risultato è null. I confronti d'ordine (<, >, <=, >=) sono l'eccezione: restituiscono false invece di null. (L'altra eccezione è bool? con & e |, dove null & false è false.) Per esempio:

Output:

True
[]
False
False
True
True

Sia a > 5 sia a <= 5 sono false, quindi !(a > 5) non equivale a a <= 5 quando a è null. Un codice come if (!(age >= 18)) Deny(); tratta un'età mancante come minorenne, il che potrebbe essere o non essere ciò che intendevi. Decidi prima cosa deve significare null, di solito con ??, e poi confronta.

L'operatore di null coalescing ??

a ?? b vale a quando a non è null, e b altrimenti. Funziona sui tipi valore nullable e sui tipi riferimento, e si concatena da sinistra a destra:

Output:

dark
16px
20
ANONYMOUS

Il lato destro viene valutato solo quando serve, quindi cache ?? LoadFromDatabase() non interroga il database quando la cache ha un valore. ?? accetta anche un'espressione throw, che permette un controllo compatto (vedi throw):

this.repository = repository ?? throw new ArgumentNullException(nameof(repository));

??= (C# 8)

x ??= y assegna y a x solo se x è null. È l'idioma per l'inizializzazione pigra:

private List<string> tags;

public void AddTag(string tag)
{
    tags ??= new List<string>();   // C# 8 and later
    tags.Add(tag);
}

Prima di C# 8 si scriveva tags = tags ?? new List<string>(); oppure if (tags == null) tags = new List<string>();.

L'operatore condizionale null ?.

a?.Member valuta Member solo se a non è null; altrimenti l'intera espressione è null. Trasforma una catena di controlli su null in un'unica espressione:

Output:

Braga
(no city)
(no customer)
0
True
handler: saved

Dettagli che vale la pena conoscere:

  • L'operatore cortocircuita il resto della catena: in nobody?.Address?.City, una volta che nobody è null non viene eseguito nulla a destra.
  • Quando il membro restituisce un tipo valore, il risultato diventa nullable: Orders?.Count è int?, ed è per questo che serve ?? 0 per farlo tornare un int.
  • Prima di C# 14 ?. non può essere la destinazione di un'assegnazione: customer?.Name = "x" non compila nelle versioni precedenti.
  • handler?.Invoke(...) è il modo standard per scatenare un evento o chiamare un callback facoltativo.

Usa ?. dove null è uno stato legittimo. Spargerlo ovunque per evitare le eccezioni nasconde il bug che ha prodotto il null inatteso, e il programma continua con dati mancanti invece di fallire dove si trova il problema.

Controllare se un valore è null

if (customer == null) { ... }             // classic
if (customer is null) { ... }             // C# 7 constant pattern
if (customer != null) { ... }
if (customer is not null) { ... }         // C# 9
if (customer is { } c) { ... }            // C# 8 property pattern: not null, and named

is null esegue sempre un vero controllo su null, mentre == chiama l'overload dell'operatore del tipo se ne ha uno, quindi un overload scritto male può far mentire x == null. Per gli argomenti che non devono essere null, fallisci subito:

public void Ship(Order order)
{
    ArgumentNullException.ThrowIfNull(order);   // .NET 6+
    // ...
}

Tipi riferimento nullable (C# 8)

Aggiungere ? a un tipo valore cambia il tipo. Aggiungere ? a un tipo riferimento, da C# 8, cambia solo ciò che il compilatore controlla. Con la funzionalità attiva (<Nullable>enable</Nullable> nel file di progetto, il default per i nuovi progetti da .NET 6), string significa "non dovrebbe mai essere null" e string? significa "può essere null", e il compilatore avvisa quando il codice non corrisponde:

#nullable enable

public class User
{
    public string Email { get; set; }      // warning CS8618: Non-nullable property 'Email'
                                           // must contain a non-null value when exiting constructor.
    public string? Nickname { get; set; }  // allowed to be null

    public int NicknameLength() => Nickname.Length;
    // warning CS8602: Dereference of a possibly null reference.

    public int SafeLength() => Nickname?.Length ?? 0;   // no warning
}

string? input = Console.ReadLine();
string name = input;                       // warning CS8600: Converting null literal or possible
                                           // null value to non-nullable type.
string checkedName = input ?? "guest";     // no warning
int len = input!.Length;                   // ! tells the compiler "trust me": no warning

Il compilatore segue i tuoi controlli lungo il metodo: dopo if (input != null), input conta come non null. L'operatore ! (null-forgiving) mette a tacere un avviso quando ne sai più dell'analisi; ognuno è un'affermazione che il compilatore non può verificare, quindi usalo di rado.

Due fatti chiariscono la maggior parte dei dubbi. Primo, sono solo avvisi: il programma gira allo stesso modo, e una string può comunque contenere null in esecuzione se arriva da codice senza annotazioni. Secondo, string? non è Nullable<string>; non ha HasValue.

Errori comuni

  • .Value senza controllo. Lancia InvalidOperationException quando è null. Usa ??, GetValueOrDefault() o is int n.
  • Pensare che !(x > 5) significhi x <= 5. Non quando x è null: ogni confronto d'ordine con null è falso.
  • Coprire ogni null con ?.. Mette a tacere il sintomo e allontana il punto di errore dalla causa.
  • Considerare rumore gli avvisi sui riferimenti nullable. Ognuno segna una riga che può lanciare NullReferenceException. Correggi il flusso o dichiara l'intenzione con ?.
  • Valori magici invece di null. -1, 0 e DateTime.MinValue per "sconosciuto" sembrano dati reali. Usa int? e DateTime?.

Domande frequenti

Cosa significa int? in C#?

int? è una scorciatoia per Nullable<int>: un int che può anche essere null. I tipi valore come int, decimal, bool e DateTime di norma contengono sempre un valore; aggiungere ? dà loro uno stato "nessun valore", che è ciò che serve per un campo di un modulo non compilato o una colonna NULL di un database. Controllalo con HasValue o != null e leggilo con Value o GetValueOrDefault().

Cosa fa ?? in C#?

a ?? b è l'operatore di null coalescing: vale a se a non è null, altrimenti b. string name = input ?? "guest"; fornisce un valore di ripiego in una sola espressione. Si può concatenare (a ?? b ?? c prende il primo valore non null) e b viene valutato solo quando serve. ??= (C# 8) assegna il lato destro solo quando la variabile è null.

Cosa fa ?. in C#?

a?.B è l'operatore condizionale null: se a è null, l'intera espressione è null e B non viene mai valutato, quindi non c'è alcuna NullReferenceException. Si può concatenare (order?.Customer?.Address?.City) e funziona per le chiamate di metodo (logger?.Log(...)), gli eventi (Changed?.Invoke(...)) e gli indicizzatori (items?[0]). Combinalo con ?? per avere un valore predefinito: order?.Customer?.Name ?? "unknown".

Cosa causa "Nullable object must have a value"?

Leggere .Value di un tipo valore nullable che è null, oppure convertirlo con un cast al tipo non nullable ((int)maybe), lancia InvalidOperationException con quel messaggio. Controlla prima HasValue, oppure usa GetValueOrDefault(), ?? fallback o un pattern: if (maybe is int n) { ... }.

Come controllo se un valore è null in C#?

Funzionano sia x == null sia x is null; is null (C# 7) non può essere influenzato da un tipo che sovraccarica ==, per questo molti progetti lo preferiscono. La negazione è x != null, oppure x is not null in C# 9. Per rifiutare argomenti null usa ArgumentNullException.ThrowIfNull(x) (.NET 6) oppure _ = x ?? throw new ArgumentNullException(nameof(x)); nel codice più vecchio.

Cosa sono i tipi riferimento nullable in C#?

Una funzionalità del compilatore di C# 8, attivata con <Nullable>enable</Nullable> (il default nei nuovi progetti .NET 6+). Quando è attiva, string significa "mai null" e string? significa "può essere null", e il compilatore avvisa quando potresti dereferenziare null (CS8602) o assegnare null a una variabile non nullable (CS8600). Cambia solo gli avvisi, non il comportamento in esecuzione.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA