Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
Tip
Novità dello sviluppo di software? Iniziare con le esercitazioni introduttive .
Esperienza in un'altra lingua? Se si lavora con i tipi nullable di Kotlin, gli optional di strictNullChecksTypeScript o Swift, il modello è familiare. C# usa l'analisi statica e la diagnostica degli avvisi anziché un tipo separato. Scorri rapidamente Esprimere l'intento con le annotazioni e Analisi dello stato null, quindi passa a Tutorial: Esprimi il tuo intento progettuale con i tipi di riferimento nullable e non nullable per applicare la funzionalità.
I tipi di riferimento nullable costituiscono un insieme di funzionalità che riducono al minimo la possibilità che il codice generi System.NullReferenceException. Si dichiarano le variabili che devono contenere null e che non sono e il compilatore avvisa quando tali dichiarazioni non corrispondono al modo in cui il codice li usa. Il comportamento di runtime del programma rimane invariato. I tipi di riferimento nullable sono esclusivamente una funzionalità disponibile in fase di compilazione.
Tre elementi costitutivi lavorano insieme:
-
Le annotazioni variabili (
stringestring?) esprimono i riferimenti destinati a consentirenull. - L'analisi dello stato Null tiene traccia del fatto che il valore di un'espressione non sia Null o forse Null in ogni punto del codice.
-
Gli attributi nelle API descrivono contratti più sfumati, ad esempio "questo argomento può essere
null, ma il valore restituito è Null solo quando l'argomento è Null".
Il compilatore combina questi segnali per produrre la diagnostica. Gli avvisi relativi a una variabile non annullabile significano che la variabile potrebbe ricevere null. Gli avvisi relativi a una variabile nullable indicano che il codice potrebbe dereferenziarla senza un controllo di null.
Dereferenziazione significa usare il valore a cui fa riferimento la variabile. Ad esempio, per chiamare un metodo su di esso (variable.Method()), leggere una proprietà (variable.Property) o indicizzarla (variable[0]). La dereferenziazione di una variabile con valore null genera un'eccezione in fase di esecuzione. Uno dei due tipi di avviso indica che il comportamento del codice non corrisponde alla progettazione dichiarata.
Contesto nullable
I progetti creati dai modelli .NET più recenti impostano <Nullable>enable</Nullable> nel file di progetto, pertanto le linee guida contenute in questo articolo si applicano così come sono scritte. Se si lavora in un progetto precedente, aprire .csproj e verificare che <PropertyGroup> contenga la riga seguente; aggiungerla se manca:
<Nullable>enable</Nullable>
Per altre informazioni sulla migrazione di un'applicazione di grandi dimensioni, vedere l'articolo sulle strategie di migrazione nullable per altre impostazioni e direttive.
Esprimi l'intento con annotazioni
Per impostazione predefinita, ogni variabile di tipo di riferimento non ammette valori null. Aggiungi ? per dichiarare un tipo di riferimento nullable:
public static void Annotations()
{
string required = "always set"; // non-nullable: assigning null produces a warning
string? optional = null; // nullable: holding null is allowed
Console.WriteLine(required.Length);
if (optional is not null)
{
Console.WriteLine(optional.Length);
}
}
L'annotazione non modifica il tipo di runtime.
string e string? sono entrambi System.String.
? Informa il compilatore della finalità di progettazione. Tale finalità modella gli avvisi generati dal compilatore:
- Una variabile che non ammette valori Null ha uno stato Null predefinito di non Null. Il compilatore avvisa se si assegna un valore che potrebbe essere
null. - Una variabile nullable ha uno stato Null predefinito di forse-null. Il compilatore avvisa se si dereferenzia la variabile senza prima controllarla.
Usare l'annotazione per rendere visibili i valori obbligatori e facoltativi nel sistema dei tipi. Il tipo seguente Person dichiara FirstName e LastName come non annullabili — ogni persona deve avere entrambe le proprietà — e MiddleName come annullabile, perché non tutti ne hanno una:
public sealed class Person(string firstName, string lastName)
{
public string FirstName { get; } = firstName;
public string? MiddleName { get; init; }
public string LastName { get; } = lastName;
public override string ToString() => MiddleName is null
? $"{FirstName} {LastName}"
: $"{FirstName} {MiddleName} {LastName}";
}
public static void DesignIntent()
{
Person p1 = new("Ada", "Lovelace") { MiddleName = "King" };
Console.WriteLine(p1);
// Output: Ada King Lovelace
Person p2 = new("Grace", "Hopper");
Console.WriteLine(p2);
// Output: Grace Hopper
}
Le annotazioni determinano l'implementazione di ToString. Poiché FirstName e LastName non ammettono valori null, l'override usa direttamente tali valori in una stringa interpolata (la sintassi $"..." che inserisce espressioni nei segnaposto {}) senza alcun controllo di valori null.
MiddleName può essere null, quindi l'override lo confronta prima con null e lo include solo quando è presente. Il compilatore impone tale distinzione: il codice che passa un valore potenzialmente nullo dove è previsto un valore non annullabile produce un avviso e anche un costruttore che lascia non inizializzato un membro non annullabile produce un avviso.
Analisi dello stato null
Il compilatore tiene traccia dello stato Null di ogni espressione. Lo stato è uno dei due valori seguenti:
-
not-null: l'espressione è nota per non essere
null. -
maybe-null: l'espressione potrebbe essere
null.
Lo stato null di una variabile locale viene aggiornato quando il compilatore analizza il codice. Due cose cambiano: assegnazioni e controlli Null. Dopo un'assegnazione, lo stato Null della variabile corrisponde all'espressione sul lato destro. Se l'espressione è null o nullable, la variabile diventa forse-null. Se l'espressione è un valore letterale non Null, la variabile diventa not-null. Dopo un controllo Null, lo stato null della variabile riflette il ramo che viene acquisito.
public static void NullStateTracking()
{
string? message = null;
// Warning: dereference of a possibly null reference.
Console.WriteLine(message.Length);
message = "Hello, World!";
// No warning: the compiler tracks that message is now not-null.
Console.WriteLine(message.Length);
}
Nell'esempio precedente, la prima dereferenziazione genera un avviso perché message è forse null. Dopo l'assegnazione a un valore letterale non Null, il compilatore sa che messagenon è null, quindi la seconda dereferenziazione è sicura.
L'analisi dei valori null si applica a controlli if, alla corrispondenza dei modelli (espressioni come is null o is { } che verificano la struttura di un valore) e al flusso di controllo che esegue cicli o termina anticipatamente:
public sealed class Node(string name)
{
public string Name { get; } = name;
public Node? Parent { get; init; }
}
public static void FlowAnalysis(Node start)
{
Node? current = start;
while (current is not null)
{
// Inside the loop, the compiler knows current is not-null.
Console.WriteLine(current.Name);
current = current.Parent;
}
}
L'analisi non esamina il contenuto dei metodi. Se ti serve un modo per comunicare lo stato di null al codice chiamante, usa gli attributi di analisi della nullabilità nella firma del metodo.
Eseguire l'override degli avvisi con !
In alcuni casi si conosce più del compilatore. L'operatore ! dichiara che un'espressione non è null, anche se l'analisi indica in caso contrario:
public static void NullForgiving()
{
// "ada" matches a switch arm that returns a non-null string,
// but the return type is string? so the compiler treats the
// result as maybe-null.
string? maybeName = LookUpName("ada");
// The ! tells the compiler "trust me, this isn't null." We just
// passed "ada", which the switch maps to "Ada Lovelace".
int length = maybeName!.Length;
Console.WriteLine(length); // => 12
}
// Returns string? because the wildcard arm yields null.
private static string? LookUpName(string id) => id switch
{
"ada" => "Ada Lovelace",
_ => null,
};
Usare ! solo in casi limitati. Ogni occorrenza è un luogo in cui il compilatore non può più proteggere l'utente. Preferire l'aggiunta di un controllo Null, la ristrutturazione del codice o l'annotazione dell'API pertinente in modo che il compilatore raggiunga autonomamente la conclusione corretta.
Attributi che descrivono i contratti API
Le annotazioni su un parametro o un tipo restituito non sono sempre sufficientemente espressive. Un metodo potrebbe accettare un argomento potenzialmente nullo, ma garantire un risultato non nullo. Un metodo di test può restituire true solo quando il relativo argomento non è Null. Usare gli attributi di analisi nullable per comunicare questi contratti:
public static bool IsPresent([NotNullWhen(true)] string? value) =>
!string.IsNullOrEmpty(value);
public static void NullAnalysisAttributes()
{
string? input = ReadInput();
if (IsPresent(input))
{
// No null-forgiving operator needed: the attribute tells the compiler
// input is not-null when IsPresent returns true.
Console.WriteLine(input.Length);
}
}
private static string? ReadInput() => "hello";
Indica NotNullWhenAttribute al compilatore che quando IsPresent restituisce true, l'argomento non è Null. All'interno del blocco if, il compilatore considera value come non nullo, senza che sia necessario l'operatore di null-forgiving. A partire da .NET 5, tutte le API di runtime .NET vengono annotate, quindi l'analisi offre vantaggi a qualsiasi codice che li chiama.
Problemi noti
Due pattern possono lasciare un riferimento non annullabile che contiene null senza alcun avviso. Entrambi i modelli sono limitazioni dell'analisi statica, non dei bug nel codice.
Struct predefiniti
È possibile creare uno struct con campi reference non annullabili utilizzando default o new(). Questo approccio lascia i campi dello struct non inizializzati:
public struct Student
{
public string FirstName;
public string? MiddleName;
public string LastName;
}
public static void DefaultStructPitfall()
{
Student s = default; // No warning, but FirstName and LastName are null.
Console.WriteLine(s.FirstName?.Length ?? -1);
}
I campi contengono null in fase di esecuzione, ma il compilatore non segnala alcun avviso. Se è necessario usare uno struct, preferire i membri obbligatori, che sono membri che il chiamante deve inizializzare tramite un inizializzatore di oggetto o un costruttore con parametri che i chiamanti devono richiamare.
Matrici di riferimenti e struct
Un nuovo array di un tipo di riferimento non annullabile contiene elementi tutti null finché non si assegna un valore a ciascuno di essi:
public static void ArrayPitfall()
{
string[] values = new string[3]; // Elements are null at run time.
Console.WriteLine(values[0]?.Length ?? -1);
string[] initialized = ["a", "b", "c"]; // Collection expression initializes every slot.
Console.WriteLine(initialized[0].Length);
}
Lo stesso problema si applica agli array di struct: ogni elemento viene inizializzato con il valore predefinito della struct, quindi i campi di riferimento non annullabili di ogni elemento sono inizializzati a null.
Inizializzare gli elementi della matrice durante la creazione della matrice.
Le espressioni di raccolta (la sintassi letterale [1, 2, 3]) e la tipizzazione basata sulla destinazione new (scrivere new() quando il compilatore può dedurne il tipo) consentono di rendere concisa l'inizializzazione completa.