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 default0
mentre 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
: restituiscetrue
se Γ¨ presente un valore,false
se Γ¨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.0
perint
).
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);
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Γ² contenerenull
temporaneamente, anche se Γ¨ non-nullable. - Il metodo
Create()
restituisce unβistanza singleton: seinstance
Γ¨null
, viene creato un nuovo oggettoVehicleService
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;