1. Introduzione
Immagina che il codice sia una pièce teatrale e le variabili siano gli attori. Ogni volta che una variabile entra in una nuova scena (blocco di codice), può essere "viva" (ha un valore) oppure un "fantasma" (null). Ma c'è un problema: lo spettatore — il compilatore — guarda la pièce non dal vivo, ma solo dal copione. Vede dove agli attori vengono assegnati i ruoli (valori), e dove spariscono (diventano null). Ma non può vedere la realtà (l'esecuzione del programma) — può solo immaginare in base al copione!
L'analisi statica del flusso dei dati è il processo in cui il compilatore controlla il codice prima che il programma venga eseguito, per capire se qualcuno sta per recitare la scena del "crollo NullReferenceException".
Il succo del processo
A partire da C# 8.0 e con l'arrivo della modalità Nullable Reference Types (NRT), il compilatore analizza se una variabile può diventare null in qualche ramo di esecuzione. E se sì — te lo dice subito. Non ti lascia "usare il nome di un attore che è diventato improvvisamente un fantasma".
2. Come leggere gli avvisi del compilatore
Categorie principali di avvisi
Quando la modalità NRT è attiva, il compilatore tiene d'occhio soprattutto le variabili di tipo reference (string, User, array, oggetti). Ecco i tipi di avvisi più comuni:
- Possible null assignment — stai cercando di assegnare un valore che potrebbe essere null a una variabile che non lo accetta.
- Dereference of a possibly null reference — stai accedendo a un membro di un oggetto, ma il compilatore non è sicuro che non sia null.
- Possible null return — un metodo potrebbe restituire null anche se dalla firma dovrebbe restituire un valore.
- Nullable object must have a value — stai usando .Value su un tipo nullable senza controllare prima.
- Unassigned non-nullable property — non hai assegnato un valore a un campo/proprietà che non accetta null.
Di solito in IDE (tipo Rider o Visual Studio) questi avvisi sono sottolineati con una linea ondulata, e passando il mouse sopra compare una spiegazione.
Esempio di avviso tipico
#nullable enable
string? possibleNull = GetString();
// Ops, il compilatore si lamenta: "Dereference of a possibly null reference"
int length = possibleNull.Length; // Avviso!
Il compilatore non è sicuro che possibleNull sia diverso da null, quindi accedere a .Length potrebbe causare un'eccezione.
Se il metodo è così:
string? GetString() { ... }
E scrivi:
string nonNullable = GetString();
Riceverai un avviso: possibile assegnamento non sicuro di null a una variabile non-nullable.
3. Il compilatore analizza il tuo codice
Esempio 1: assegnamento diretto
string? name = null;
Console.WriteLine(name.Length); // Avviso: possibile accesso a null
Qui è ovvio: name è null, quindi provare ad accedere a una proprietà è potenzialmente pericoloso.
Esempio 2: controllo su null
string? name = GetUserName();
if (name != null)
{
Console.WriteLine(name.Length); // Tutto ok!
}
else
{
Console.WriteLine("Nome non specificato!");
}
Il compilatore capisce che dentro il blocco if (name != null) la variabile è sicura. Questo è il flow analysis — analisi del flusso dei valori in base alle condizioni.
4. Il compilatore non può guardare "dentro il metodo"
Guarda solo la firma — se c'è scritto che il metodo restituisce string?, crede che possa essere null, anche se dentro restituisce sempre una stringa.
Esempio 3: ritorno di un valore da un metodo
string? GetMaybeName(bool useName)
{
if (useName)
return "Code Jedi";
else
return null;
}
void PrintLength()
{
string? name = GetMaybeName(false);
Console.WriteLine(name.Length); // Avviso!
}
Il compilatore vede che GetMaybeName può restituire null, quindi ti chiede di stare attento.
5. Scenari comuni e avvisi
Assegnamento di nullable a variabile non-nullable
string? maybeUser = GetUser();
string alwaysUser = maybeUser; // warning: possible null assignment
Per eliminare l'avviso:
string alwaysUser = maybeUser ?? "Ospite";
Così ci sarà sempre una stringa — o dal metodo, o "Ospite".
Dimenticato di controllare .HasValue su un value-type nullable
int? age = GetAge();
int realAge = age.Value; // warning: possibile accesso a null
Devi fare così:
if (age.HasValue)
Console.WriteLine(age.Value);
Oppure:
int realAge = age ?? -1;
Proprietà automatiche senza inizializzazione
class User
{
public string Name { get; set; } // warning: non inizializzato!
}
Soluzioni:
public string Name { get; set; } = "Senza nome";
oppure
public string? Name { get; set; }
6. Casi complessi di analisi del flusso
Analisi nei cicli
string? text = null;
while (text == null)
{
text = Console.ReadLine();
}
Console.WriteLine(text.Length); // tutto ok: il compilatore ha capito!
Diversi percorsi di assegnamento
string? name;
if (Random.Shared.Next() % 2 == 0)
name = "Alice";
else
name = null;
Console.WriteLine(name.Length); // warning: possible null dereference
Il compilatore analizza tutti i rami. Se anche in uno solo c'è null — ti avvisa.
7. Come "calmare" il compilatore (e quando non dovresti farlo)
A volte sei sicuro che il valore non può essere null, ma il compilatore non ti crede:
string? value = GetSomething();
if (value == null)
throw new Exception("Valore atteso!");
Console.WriteLine(value.Length); // L'avviso sparisce
Oppure usi l'operatore !:
string? value = GetSomething();
Console.WriteLine(value!.Length); // Nessun avviso, ma potenzialmente pericoloso
Questo si chiama null-forgiving operator. Non controlla nulla — dice solo al compilatore: "tranquillo, è tutto sotto controllo". Ma se sbagli — a runtime becchi un'eccezione.
GO TO FULL VERSION