Questa nota prende a piene mani dal corso # From Zero to Hero: Working with Null in C#

Prima di C# 2.0, non era possibile assegnare null a tipi valore (es. int, short, long, ecc.), in quanto ciΓ² generava un errore di compilazione. Con C# 2.0+, Γ¨ stata introdotta la struct Nullable<T>, che permette di wrappare un tipo valore affinchΓ© possa rappresentare anche l’assenza di valore (null), ad esempio:

Nullable<int> count = null;

È stato inoltre introdotto uno zucchero sintattico per semplificare la scrittura:

int? count = null;

int? Γ¨ quindi un alias compatto per Nullable<int>.

Comportamenti importanti:

  • Il tipo int ha come valore di default 0 mentre il tipo int? ha come valore di default null.
  • È possibile inizializzare un nullable in modo implicito:
int? x = new(); // x sarΓ  null
  • La struct Nullable<T> espone alcune proprietΓ /metodi utili:
    • HasValue: restituisce true se Γ¨ presente un valore, false se Γ¨ null.
    • Value: restituisce il valore sottostante (attenzione: solleva eccezione se HasValue Γ¨ false).
    • GetValueOrDefault(): restituisce il valore sottostante se presente, altrimenti il valore di default del tipo T (es. 0 per int).

Nullable Aware Context

I reference type (es. string) possono giΓ  essere null da sempre, ma il compilatore non dava alcun avviso o aiuto se si dimenticava di gestire il null, con il rischio di NullReferenceException a runtime. In C# 8 Γ¨ stato introdotto il Nullable Aware Context, abilita un contesto in cui il compilatore analizza il codice per aiutare a evitare errori da null non gestiti. Si puΓ² attivare per progetto:

<Nullable>enable</Nullable>  // nel file .csproj

Oppure nel singolo file C#:

#nullable enable

In particolare ora se un reference type non Γ¨ dichiarato con il ? il compilatore fornirΓ  warning se non Γ¨ gestito il fatto che questa variabile possa essere null. Il ? su un reference type serve quindi solo per i developer e per il compilatore e indica che una variabile ci si aspetti che possa diventare null (string?) oppure che non dovrebbe mai essere null (string). L’obiettivo Γ¨ fare in modo che il compilatore ti aiuta a prevenire i NullReferenceException segnalando dove potresti avere null non gestiti. Questa Γ¨ una verifica a livello di compilatore, non cambia il comportamento a runtime: string puΓ² ancora essere null a runtime, sia che sia definito string che string?.

Nullable value type vs nullable reference type

La differenza tra un nullable value type e un nullable reference type in C# Γ¨ sostanziale sia concettualmente che dal punto di vista dell’implementazione. Quando si parla di int?, cioΓ¨ un tipo valore nullable, ci si riferisce in realtΓ  a una struttura Nullable<T>, che internamente contiene due elementi: una proprietΓ  HasValue, che indica se Γ¨ presente un valore, e una proprietΓ  Value, che rappresenta il valore effettivo. Anche quando il tipo Γ¨ null, non si tratta di un riferimento assente, ma di una struct che contiene un flag per indicare l’assenza del valore. Al contrario, Person?, che rappresenta un nullable reference type, Γ¨ semplicemente un riferimento che puΓ² essere null. Non c’è alcun wrapper o struttura a supporto: ==il tipo Γ¨ lo stesso Person, ma il punto interrogativo viene usato esclusivamente per abilitare i controlli del compilatore introdotti con il nullable aware context==. A livello di runtime, Person? Γ¨ del tutto identico a Person, con la differenza che il compilatore tiene traccia delle possibili assegnazioni null e genera warning se si tenta di usare un valore senza verificarne la presenza. Tecnicamente, i nullable value types vivono nello stack e solo in caso di boxing vengono allocati su heap. I nullable reference types, invece, sono normali riferimenti che possono semplicemente contenere null, senza alcuna struttura aggiuntiva. La gestione della nullabilitΓ  in questo caso Γ¨ tutta a carico del compilatore, che segnala potenziali dereferenziazioni pericolose, ma non aggiunge alcuna protezione automatica a runtime.

int value = 42;
int? nullableValue = default;
Console.WriteLine(nullableValue.HasValue);
 
Person person = new() { Name = "Alice", Age = 25 };
Person? nullablePerson = default;
Console.WriteLine(nullablePerson?.Name);
VariabileStackHeap
value42-
nullableValue?0x0001Nullable<int> { HasValue = false, Value = 0 }
person0x0002Person { Name = "Alice", Age = 25 }
nullablePerson?null- (riferimento nullo, non c’è struct wrapper)

Attributi

[AllowNull]

In alcuni casi posso evitare il warning sull fatto che un non nullable type puΓ² avere valori null dicendo al compilatore che Γ¨ ammesso che la variabile possa contenere null in alcuni momenti del ciclo di vita dell’applicazione (es. durante l’inizializzazione). Prendiamo questo esempio:

public class VehicleService
{
    [AllowNull]
    private static VehicleService instance;
 
    private VehicleService() { }
 
    public static VehicleService Create() => instance ??= new VehicleService();
}
  • Il campo instance Γ¨ dichiarato come non nullable, ma inizialmente vale null. Per evitare warning del compilatore, si usa [AllowNull] per dire esplicitamente che il campo puΓ² contenere null temporaneamente, anche se Γ¨ non-nullable.
  • Il metodo Create() restituisce un’istanza singleton: se instance Γ¨ null, viene creato un nuovo oggetto VehicleService usando l’operatore ??= (assegna solo se Γ¨ null).
  • Sono sicuro che all’esterno questa variabile non sarΓ  mai null in quanto l’unico metodo public per crearla Γ¨ Create() che la inizializza.

In questo caso avere che VehicleService Γ¨ null all’inizio Γ¨ una scelta progettuale consapevole: il campo Γ¨ inizialmente null e verrΓ  inizializzato lazy (al primo accesso tramite ??=). Quindi, non c’è errore logico, ma il compilatore lo segnala comunque perchΓ© il contratto del tipo dice β€œnon puΓ² mai essere null”. L’attributo [AllowNull] permette di dire β€œquesto campo puΓ² temporaneamente essere null anche se Γ¨ dichiarato come non nullable, ed Γ¨ voluto”, senza cambiare la sua firma (cioΓ¨ senza doverlo dichiarare VehicleService?).

[DisallowNull]

Fa l’opposto di [AllowNull]: impedisce che venga assegnato null anche se la proprietΓ  o campo Γ¨ dichiarato come nullable (string?).

[DisallowNull]
public string? Description { get; set; }

Qui Description Γ¨ nullable, ma non puΓ² ricevere null in fase di assegnazione. PuΓ² perΓ² essere null in lettura.

[MemberNotNull(nameof(attribute))]

L’attributo [MemberNotNull(nameof(PropertyName))] assicura al compilatore che, dopo l’esecuzione del metodo su cui Γ¨ associato, la proprietΓ  PropertyName sarΓ  sicuramente non nulla. Serve per migliorare l’analisi statica della nullabilitΓ  e prevenire warning.

public class VehicleService
{
    private static VehicleService? instance;
    public static DateTime? CreatedOn { get; private set; }
 
    [MemberNotNull(nameof(CreatedOn))]
    public static VehicleService Create()
    {
        CreatedOn = DateTime.UtcNow;
        return instance ??= new VehicleService();
    }
}

[return: NotNullIfNotNull("param")]

L’attributo [return: NotNullIfNotNull("param")] dice al compilatore che il metodo su cui Γ¨ associato restituirΓ  un valore non nullo solo se anche il parametro param non Γ¨ nullo. Aiuta l’analisi di nullabilitΓ  a propagare correttamente le informazioni.

Codice completo:

public class VehicleService
{
    [return: NotNullIfNotNull("vehicle")]
    public Vehicle? UpgradeVehicle(Vehicle? vehicle)
    {
        if (vehicle == null) return null;
        return new Vehicle(vehicle.Id, vehicle.Make, vehicle.Model, vehicle.Color);
    }
}

Warning comuni

Converting null literal or possible null value to non-nullable type

Il compilatore sta segnalando che stai assegnando un valore potenzialmente null (o un null literal che corrisponde alla stringa null o un possible null value che corrisponde ad una funzione che puΓ² ritornare null) a un tipo dichiarato come non nullable, cosa che potrebbe portare a un NullReferenceException a runtime. Esempio:

// 
Vehicle v = null;             // Null literal
Vehicle v = GetVehicle();     // Possible null value
string color = v.Color;
Vehicle? GetVehicle() => null;

Dereference of a possibly null reference

Significa che il compilatore rileva che v Γ¨ di tipo Vehicle? (nullable), quindi potrebbe essere null, eppure stai cercando di accedere direttamente alla sua proprietΓ  .Color senza controllare prima. Se v fosse effettivamente null a runtime, otterresti una NullReferenceException. La soluzione Γ¨ controllare che v != null prima dell’accesso.

string color = v?.Color ?? defaultVehicle.Color;