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
intha come valore di default0mentre il tipoint?ha come valore di defaultnull. - Γ possibile inizializzare un nullable in modo implicito:
int? x = new(); // x sarΓ null- La struct
Nullable<T>espone alcune proprietà /metodi utili:HasValue: restituiscetruese è presente un valore,falsese ènull.Value: restituisce il valore sottostante (attenzione: solleva eccezione seHasValueèfalse).GetValueOrDefault(): restituisce il valore sottostante se presente, altrimenti il valore di default del tipo T (es.0perint).
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 .csprojOppure nel singolo file C#:
#nullable enableIn 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);| Variabile | Stack | Heap |
|---|---|---|
value | 42 | - |
nullableValue? | 0x0001 | Nullable<int> { HasValue = false, Value = 0 } |
person | 0x0002 | Person { 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 valenull. Per evitare warning del compilatore, si usa[AllowNull]per dire esplicitamente che il campo può contenerenulltemporaneamente, anche se è non-nullable. - Il metodo
Create()restituisce unβistanza singleton: seinstanceΓ¨null, viene creato un nuovo oggettoVehicleServiceusando lβoperatore??=(assegna solo se Γ¨null). - Sono sicuro che allβesterno questa variabile non sarΓ mai null in quanto lβunico metodo
publicper 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;