Risolvere gli avvisi nullabili

Tip

Non conosci ancora i tipi riferimento nullable? Leggi prima i tipi di riferimento nullable per comprendere le annotazioni e l'analisi dello stato null. Questo articolo presuppone che vengano visualizzati avvisi in un progetto in cui la funzionalità è abilitata.

Cerchi uno specifico codice di errore del compilatore? L'articolo di riferimento Risolvere gli avvisi relativi ai tipi nullable elenca ogni avviso CS86xx con la tecnica corrispondente.

Quando si abilitano i tipi riferimento nullable, il compilatore genera avvisi ovunque il comportamento del codice non corrisponda alle relative annotazioni. La maggior parte degli avvisi rientra in un piccolo set di modelli. Dopo aver riconosciuto il modello, la correzione è in genere una delle cinque tecniche seguenti:

  • Aggiungere un controllo Null.
  • Aggiungere o rimuovere un'annotazione ? o ! .
  • Aggiungere un attributo che descrive il contratto Null.
  • Inizializzare correttamente le variabili.
  • Verificare l'impostazione del progetto.

Questo articolo illustra ogni tecnica con un esempio rappresentativo. L'obiettivo non è quello di disattivare gli avvisi. Serve a rendere esplicito l'intento del codice nella gestione dei valori null, in modo che il compilatore arrivi alle tue stesse conclusioni.

Stato null: ciò di cui il compilatore tiene traccia

Prima di esaminare le tecniche, è utile sapere in che modo il compilatore tiene traccia di potenziali violazioni dello stato Null. Durante la lettura del codice, il compilatore tiene traccia dello stato Null di ogni espressione: l'analisi relativa all'eventuale presenza null dell'espressione in quel punto del codice. Lo stato Null è uno dei due valori seguenti:

  • not-null — il compilatore può dimostrare che qui l'espressione non è null. È possibile usarlo in modo sicuro senza un controllo.
  • maybe-null : il compilatore non può escludere null. L'uso dell'espressione senza verificarlo genera un avviso.

Lo stato null di una variabile cambia man mano che il compilatore segue il codice. Un metodo che potrebbe restituire null produce un risultato forse null . Un controllo su if (x is not null) restringe x a not-null all'interno del blocco if. Gli avvisi visualizzati sono avvisi con cui il compilatore segnala di aver stabilito che un'espressione è in uno stato maybe-null e che la si sta per usare come se fosse not-null. Ogni tecnica nel resto di questo articolo è un modo diverso per fornire al compilatore le informazioni necessarie per assicurarsi che un'espressione non sia null prima di usarla.

Aggiungere un controllo null

L'avviso più comune è la possibile dereferenziazione di null. Il compilatore ha rilevato lo stato Null di una variabile su forse-null e ha visto la variabile usata senza un controllo:

public static int LengthOfMessageUnsafe(string? message)
{
    // Warning CS8602: dereference of a possibly null reference.
    return message.Length;
}

La correzione è in genere una clausola di guardia. Una guard clause è un controllo all'inizio di un metodo o di un blocco che restituisce o solleva un'eccezione se un input non è valido. Solo il percorso sicuro continua. Dopo l'esecuzione del controllo, il compilatore aggiorna lo stato Null della variabile a not-null nel percorso sicuro:

public static int DereferenceFixed(string? message)
{
    if (message is null)
    {
        return 0;
    }

    // No warning: the compiler knows message is not-null on this path.
    return message.Length;
}

Corrispondenza di modelli (espressioni come is null o is { } che verificano la forma di un valore), ?? e ??= includono controlli null:

public static int NullOperatorsFix(string? message)
{
    // ?. evaluates to null if message is null; ?? supplies the fallback value.
    int length = message?.Length ?? 0;

    // Pattern matching narrows the type on the matching branch.
    if (message is { Length: > 0 })
    {
        length = message.Length;
    }

    return length;
}

Il modello di proprietà { Length: > 0 } trova corrispondenza solo se message è diverso da null e la proprietà Length è maggiore di zero, quindi il compilatore considera messagenon nullo all'interno del blocco if. Un test più semplice is not null produce lo stesso restringimento dello stato Null senza esaminare alcuna proprietà.

Per una panoramica approfondita degli operatori, vedere Operatori Null.

Regolare le annotazioni

Il compilatore avvisa anche quando il tuo codice assegna un'espressione potenzialmente null a una variabile che non ammette valori null. Questo avviso indica una delle due cose seguenti:

  • La variabile deve consentire valori Null. In tal caso, aggiungi un ? al tipo.
  • L'espressione non produce mai un valore Null. Annotare l'API che l'ha prodotta.
public static void AssignmentWarning()
{
    // Warning CS8600: converting null literal or possible null value to non-nullable type.
    string name = Lookup("nobody");
    Console.WriteLine(name);
}

Se Lookup restituisce legittimamente null, modificare il sito di chiamata in modo da accettare il valore mancante:

public static void AssignmentFixed()
{
    string? name = Lookup("somebody");
    if (name is not null)
    {
        Console.WriteLine(name);
    }
}

Se Lookup non restituisce mai null, modificare la firma affinché restituisca un tipo di riferimento non annullabile. Gli scenari in cui lo stato Null del valore restituito dipende dall'input, vedere la sezione seguente sugli attributi di analisi Null.

Utilizzare l'operatore null-forgiving ! solo quando è possibile garantire che un valore non sia null, ma non è possibile esprimere tale garanzia nel sistema di tipi. Ogni ! è una posizione in cui il compilatore non può più proteggere l'utente, quindi preferire l'aggiunta di un controllo o l'annotazione dell'API di origine.

Aggiungere un attributo di analisi null

A volte la correzione corretta non si trova nel sito di chiamata. La firma di un metodo non acquisisce la relazione tra gli input e gli output con precisione sufficiente e il compilatore genera avvisi all'interno di codice altrimenti sicuro:

public static bool IsPresent(string? text) =>
    !string.IsNullOrEmpty(text);

public static void CallerWithoutAttribute(string? text)
{
    if (IsPresent(text))
    {
        // Warning CS8602: dereference of a possibly null reference.
        // The signature doesn't tell the compiler text is not-null here.
        Console.WriteLine(text.Length);
    }
}

Il corpo di IsPresent dimostra che l'argomento non è Null quando il metodo restituisce true, ma la firma non lo dice. Aggiungere un attributo di analisi nullable per rendere il contratto parte dell'API:

public static bool AttributedIsPresent([NotNullWhen(true)] string? text) =>
    !string.IsNullOrEmpty(text);

public static void CallerWithAttribute(string? text)
{
    if (AttributedIsPresent(text))
    {
        // No warning: the attribute tells the compiler text is not-null.
        Console.WriteLine(text.Length);
    }
}

Gli attributi comuni includono:

L'elenco completo si trova in Attributi nullable per l'analisi statica.

Inizializza membri non annullabili

Un avviso del costruttore indica che un campo, una proprietà o una proprietà automatica che non ammette valori Null (una proprietà che usa il campo sottostante generato dal compilatore, ad esempio public string Name { get; set; }) esce dal costruttore senza che le venga assegnato un valore non Null:

public class PersonUninitialized
{
    // Warning CS8618: Non-nullable property 'Name' is uninitialized.
    public string Name { get; set; }
}

Hai diversi modi per risolverlo. Selezionare quello che meglio corrisponde alla finalità di progettazione.

Rendere obbligatorio il valore come argomento del costruttore. Usare un costruttore primario (parametri dichiarati sul tipo stesso, disponibili in tutto il corpo) o un costruttore regolare che inizializza la proprietà:

public class PersonInjected(string name)
{
    public string Name { get; } = name;
}

Impostare la proprietà required. Il chiamante deve inizializzarlo tramite un inizializzatore di oggetto (la { Property = value } sintassi seguente new):

public class PersonRequired
{
    public required string Name { get; init; }
}

Inizializzare con un valore predefinito. Quando il tipo ha un valore vuoto significativo, inizializzarlo al momento della dichiarazione:

public class PersonInitialized
{
    public string Name { get; set; } = "John Doe";
}

Tip

Scegliere questa tecnica solo quando il tipo ha un valore predefinito veramente valido: uno che è un'istanza valida e completamente funzionale per i chiamanti da utilizzare. Gli esempi includono raccolte vuote. Non inventare un valore sentinella (un valore segnaposto come String.Empty, "N/A", "unknown" o -1 che viene trattato come "nessun valore") al posto di null: sopprime l'avviso, ma ogni chiamante deve conoscere e verificare il valore sentinella e il sistema dei tipi non può aiutare. Se non esiste un valore predefinito valido, rendere invece la proprietà nullable.

Rendere nullable la proprietà. Quando il valore potrebbe essere effettivamente mancante, modificare il tipo in nullable:

public class PersonOptional
{
    public string? Name { get; set; }
}

Se un metodo helper inizializza il membro, annotare l'helper con MemberNotNullAttribute in modo che il compilatore possa accreditarvi le chiamate.

Verificare l'impostazione del progetto

I nuovi progetti in C# abilitano i tipi di riferimento nullable per impostazione predefinita, quindi la maggior parte del codice che scrivi o leggi ha già questa funzionalità abilitata. In genere non è necessario configurare nulla. Se si è curiosi di sapere se un progetto è abilitato o se è necessario modificare l'impostazione, cercare l'elemento <Nullable> in .csproj:

<PropertyGroup>
  <Nullable>enable</Nullable>
</PropertyGroup>

I valori supportati comuni sono enable (impostazione predefinita per i nuovi progetti) e disable. Se l'elemento non è presente, il progetto usa qualsiasi impostazione predefinita dell'SDK e del framework di destinazione.

Se è necessario abilitare nullable solo per una parte di un file con #nullable direttive o usare le modalità parziali warnings e annotations durante la migrazione di una codebase esistente, vedere Strategie di migrazione nullable.

Cosa fare dopo

Quando un avviso non rientra in nessuno di questi schemi, l'articolo di riferimento Risolvere gli avvisi nullable indica la tecnica per ogni avviso CS86xx generato dal compilatore.

Per pianificare una migrazione che introduca progressivamente i tipi riferimento nullable in una base di codice esistente, vedi Strategie di migrazione per i tipi nullable.