Attributi per l'analisi statica con stato Null interpretati dal compilatore C#

In un contesto abilitato nullable, il compilatore esegue l'analisi statica del codice per determinare lo stato Null di tutte le variabili di tipo riferimento:

  • not-null: l'analisi statica determina che una variabile ha un valore non Null.
  • maybe-null: l'analisi statica non è in grado di determinare che a una variabile viene assegnato un valore non Null.

Questi stati consentono al compilatore di fornire avvisi quando è possibile dereferenziare un valore Null, generando un'eccezione System.NullReferenceException. Questi attributi forniscono al compilatore informazioni semantiche sullo stato Null degli argomenti, dei valori restituiti e dei membri dell'oggetto in base allo stato degli argomenti e dei valori restituiti. Il compilatore fornisce avvisi più accurati quando le API sono state annotate correttamente con queste informazioni semantiche.

Questo articolo fornisce una breve descrizione di ognuno degli attributi del tipo riferimento nullable e come usarli.

Iniziamo con un esempio. Si supponga che la libreria disponga dell'API seguente per recuperare una stringa di risorsa. Questo metodo è stato originariamente compilato in un contesto oblivious nullable:

bool TryGetMessage(string key, out string message)
{
    if (_messageMap.ContainsKey(key))
        message = _messageMap[key];
    else
        message = null;
    return message != null;
}

L'esempio precedente segue il modello familiare Try* in .NET. Esistono due parametri di riferimento per questa API: key e message. Questa API ha le regole seguenti relative allo stato Null di questi parametri:

  • I chiamanti non devono passare null come argomento per key.
  • I chiamanti possono passare una variabile il cui valore è null come argomento per message.
  • Se il metodo TryGetMessage restituisce true, il valore di message non è Null. Se il valore restituito è false,, il valore di message è Null.

La regola per key può essere espressa in modo conciso: key deve essere un tipo riferimento non nullable. Il parametro message è più complesso. Consente una variabile che è null come argomento, ma garantisce che, in caso di esito positivo, l'argomento out non è null. Per questi scenari, è necessario un vocabolario più completo per descrivere le aspettative. L'attributo NotNullWhen descritto di seguito descrive lo stato Null per l'argomento usato per il parametro message.

Nota

L'aggiunta di questi attributi fornisce al compilatore altre informazioni sulle regole per l'API. Quando il codice chiamante viene compilato in un contesto abilitato nullable, il compilatore avvisa i chiamanti quando violano tali regole. Questi attributi non abilitano altri controlli sull'implementazione.

Attributo Category Significato
AllowNull Precondizione Un parametro, un campo o una proprietà non nullable può essere Null.
DisallowNull Precondizione Un parametro, un campo o una proprietà nullable non deve mai essere Null.
MaybeNull Postcondizione Un parametro, un campo, una proprietà o un valore restituito non nullable può essere Null.
NotNull Postcondizione Un parametro, un campo, una proprietà o un valore restituito nullable non sarà mai Null.
MaybeNullWhen Postcondizione condizionale Un argomento non nullable può essere Null quando il metodo restituisce il valore specificato bool.
NotNullWhen Postcondizione condizionale Un argomento nullable non sarà Null quando il metodo restituisce il valore specificato bool.
NotNullIfNotNull Postcondizione condizionale Un valore restituito, una proprietà o un argomento non è Null se l'argomento per il parametro specificato non è Null.
MemberNotNull Metodi di supporto per metodi e proprietà Il membro elencato non sarà Null quando il metodo restituisce.
MemberNotNullWhen Metodi di supporto per metodi e proprietà Il membro elencato non sarà Null quando il metodo restituisce il valore specificato bool.
DoesNotReturn Codice non raggiungibile Un metodo o una proprietà non restituisce mai. In altre parole, genera sempre un'eccezione.
DoesNotReturnIf Codice non raggiungibile Questo metodo o proprietà non restituisce mai se il parametro bool associato ha il valore specificato.

Le descrizioni precedenti sono un riferimento rapido al funzionamento di ogni attributo. Le sezioni seguenti descrivono il comportamento e il significato di questi attributi in modo più approfondito.

Precondizioni: AllowNull e DisallowNull

Si consideri una proprietà di lettura/scrittura che non restituisce mai null perché ha un valore predefinito ragionevole. I chiamanti passano null alla funzione di accesso set quando lo impostano su tale valore predefinito. Si consideri, ad esempio, un sistema di messaggistica che richiede un nome di schermata in una chat room. Se non ne viene specificato nessuno, il sistema genera un nome casuale:

public string ScreenName
{
    get => _screenName;
    set => _screenName = value ?? GenerateRandomScreenName();
}
private string _screenName;

Quando si compila il codice precedente in un contesto oblivious nullable, va tutto bene. Dopo aver abilitato i tipi riferimento nullable, la proprietà ScreenName diventa un riferimento non nullable. Risposta corretta per la funzione di accesso get: non restituisce mai null. I chiamanti non devono controllare la proprietà restituita per null. Ma ora l'impostazione della proprietà su null genera un avviso. Per supportare questo tipo di codice, aggiungere l'attributo System.Diagnostics.CodeAnalysis.AllowNullAttribute alla proprietà, come illustrato nel codice seguente:

[AllowNull]
public string ScreenName
{
    get => _screenName;
    set => _screenName = value ?? GenerateRandomScreenName();
}
private string _screenName = GenerateRandomScreenName();

Potrebbe essere necessario aggiungere una direttiva using per System.Diagnostics.CodeAnalysis per usare questo e altri attributi descritti in questo articolo. L'attributo viene applicato alla proprietà, non alla funzione di accesso set. L'attributo AllowNull specifica le pre-condizionie si applica solo agli argomenti. La funzione di accesso get ha un valore restituito, ma nessun parametro. Pertanto, l'attributo AllowNull si applica solo alla funzione di accesso set.

Nell'esempio precedente viene illustrato cosa cercare quando si aggiunge l'attributo AllowNull in un argomento:

  1. Il contratto generale per tale variabile è che non deve essere null, quindi si vuole un tipo di riferimento non nullable.
  2. Esistono scenari per il passaggio di null da parte di un chiamante come argomento, anche se non sono l'utilizzo più comune.

Nella maggior parte dei casi è necessario questo attributo per le proprietà o gli argomenti in, out e ref. L'attributo AllowNull è la scelta migliore quando una variabile è in genere non Null, ma è necessario consentire null come precondizione.

In contrasto con gli scenari di utilizzo di DisallowNull: si usa questo attributo per specificare che un argomento di un tipo riferimento nullable non deve essere null. Si consideri una proprietà in cui null è il valore predefinito, ma i client possono impostarlo solo su un valore non Null. Osservare il codice seguente:

public string ReviewComment
{
    get => _comment;
    set => _comment = value ?? throw new ArgumentNullException(nameof(value), "Cannot set to null");
}
string _comment;

Il codice precedente è il modo migliore per esprimere la progettazione che ReviewComment potrebbe essere null, ma non può essere impostato su null. Una volta che questo codice è in grado di rendere nullable, è possibile esprimere questo concetto in modo più chiaro ai chiamanti che usano System.Diagnostics.CodeAnalysis.DisallowNullAttribute:

[DisallowNull]
public string? ReviewComment
{
    get => _comment;
    set => _comment = value ?? throw new ArgumentNullException(nameof(value), "Cannot set to null");
}
string? _comment;

In un contesto nullable, la funzione di accesso ReviewCommentget potrebbe restituire il valore predefinito di null. Il compilatore avvisa che deve essere controllato prima dell'accesso. Inoltre, avvisa i chiamanti che, anche se potrebbe essere null, i chiamanti non devono impostarlo in modo esplicito su null. L'attributo DisallowNull specifica anche una condizione preliminare, non influisce sulla funzione di accesso get. L'attributo DisallowNull viene usato quando si osservano queste caratteristiche relative a:

  1. La variabile può essere null negli scenari principali, spesso quando viene creata una prima istanza.
  2. La variabile non deve essere impostata in modo esplicito su null.

Queste situazioni sono comuni nel codice originariamente null oblivious. È possibile che le proprietà dell'oggetto siano impostate in due operazioni di inizializzazione distinte. È possibile che alcune proprietà siano impostate solo dopo il completamento di alcune operazioni asincrone.

Gli attributi AllowNull e DisallowNull consentono di specificare che le precondizioni sulle variabili potrebbero non corrispondere alle annotazioni nullable su tali variabili. Questi forniscono maggiori dettagli sulle caratteristiche dell'API. Queste informazioni aggiuntive consentono ai chiamanti di usare correttamente l'API. Tenere presente che si specificano precondizioni usando gli attributi seguenti:

  • AllowNull: un argomento non nullable può essere Null.
  • DisallowNull: un argomento nullable non deve mai essere null.

Postcondizioni: MaybeNull e NotNull

Si supponga di avere un metodo con la firma seguente:

public Customer FindCustomer(string lastName, string firstName)

È probabile che sia stato scritto un metodo simile a questo per restituire null quando il nome cercato non è stato trovato. null indica chiaramente che il record non è stato trovato. In questo esempio è probabile che si modifichi il tipo restituito da Customer a Customer?. La dichiarazione del valore restituito come tipo riferimento nullable specifica chiaramente la finalità di questa API:

public Customer? FindCustomer(string lastName, string firstName)

Per motivi trattati in Generics nullability, questa la tecnica potrebbe non produrre l'analisi statica corrispondente all'API. Per motivi trattati in Generics nullability, questa tecnica potrebbe non produrre l'analisi statica che corrisponde alla vostra API. Potrebbe essere disponibile un metodo generico che segue un modello simile:

public T Find<T>(IEnumerable<T> sequence, Func<T, bool> predicate)

Il metodo restituisce null quando l'elemento cercato non viene trovato. È possibile chiarire che il metodo restituisce null quando un elemento non viene trovato aggiungendo l'annotazione MaybeNull al metodo restituito:

[return: MaybeNull]
public T Find<T>(IEnumerable<T> sequence, Func<T, bool> predicate)

Il codice precedente informa i chiamanti che il valore restituito può effettivamente essere Null. Informa inoltre il compilatore che il metodo può restituire un'espressione null anche se il tipo non è nullable. Quando si dispone di un metodo generico che restituisce un'istanza del relativo parametro di tipo, T, è possibile esprimere che non restituisce mai null usando l'attributo NotNull.

È anche possibile specificare che un valore restituito o un argomento non è Null anche se il tipo è un tipo riferimento nullable. Il metodo seguente è un metodo helper che genera se il primo argomento è null:

public static void ThrowWhenNull(object value, string valueExpression = "")
{
    if (value is null) throw new ArgumentNullException(nameof(value), valueExpression);
}

È possibile chiamare questa routine come segue:

public static void LogMessage(string? message)
{
    ThrowWhenNull(message, $"{nameof(message)} must not be null");

    Console.WriteLine(message.Length);
}

Dopo aver abilitato i tipi riferimento Null, ci si vuole assicurare che il codice precedente venga compilato senza avvisi. Quando il metodo viene restituito, è garantito che il parametro value non è Null. Tuttavia, è accettabile chiamare ThrowWhenNull con un riferimento Null. È possibile rendere value un tipo riferimento nullable, e aggiungere la post-condizione NotNull alla dichiarazione di parametro:

public static void ThrowWhenNull([NotNull] object? value, string valueExpression = "")
{
    _ = value ?? throw new ArgumentNullException(nameof(value), valueExpression);
    // other logic elided

Il codice precedente esprime chiaramente il contratto esistente: i chiamanti possono passare una variabile con il valore null, ma l'argomento non sarà mai null se il metodo restituisce senza generare un'eccezione.

È possibile specificare postcondizioni non condizionali usando gli attributi seguenti:

  • MaybeNull: un valore restituito non nullable può essere Null.
  • NotNull: un valore restituito nullable non sarà mai null.

Post-condizioni condizionali: NotNullWhen, MaybeNullWhen e NotNullIfNotNull

Probabilmente si ha familiarità con il metodo stringString.IsNullOrEmpty(String). Questo metodo restituisce true quando l'argomento è null o una stringa vuota. Si tratta di una forma di controllo null: i chiamanti non devono controllare null-check l'argomento se il metodo restituisce false. Per rendere un metodo simile al seguente, è necessario impostare l'argomento su un tipo riferimento nullable e aggiungere l'attributo NotNullWhen:

bool IsNullOrEmpty([NotNullWhen(false)] string? value)

Questo informa il compilatore che qualsiasi codice in cui il valore restituito è false non ha bisogno di controlli su Null. L'aggiunta dell'attributo informa l'analisi statica del compilatore che IsNullOrEmpty esegue il controllo Null necessario: quando restituisce false, l'argomento non è null.

string? userInput = GetUserInput();
if (!string.IsNullOrEmpty(userInput))
{
    int messageLength = userInput.Length; // no null check needed.
}
// null check needed on userInput here.

Nota

L'esempio precedente è valido solo in C# 11 e versioni successive. A partire da C# 11, l'nameofespressione può fare riferimento ai nomi dei parametri di parametro e di tipo quando viene usato in un attributo applicato a un metodo. In C# 10 e versioni precedenti è necessario usare un valore letterale stringa anziché l'espressione nameof.

Il metodo String.IsNullOrEmpty(String) verrà annotato come illustrato in precedenza per .NET Core 3.0. È possibile che nella codebase siano presenti metodi simili che controllano lo stato degli oggetti per i valori Null. Il compilatore non riconoscerà i metodi di controllo Null personalizzati e sarà necessario aggiungere manualmente le annotazioni. Quando si aggiunge l'attributo, l'analisi statica del compilatore sa quando la variabile testata è stata verificata su Null.

Un altro uso per questi attributi è il modello di Try*. Le postcondizioni per ref e gli argomenti out vengono comunicati tramite il valore restituito. Si consideri questo metodo illustrato in precedenza (in un contesto disabilitato nullable):

bool TryGetMessage(string key, out string message)
{
    if (_messageMap.ContainsKey(key))
        message = _messageMap[key];
    else
        message = null;
    return message != null;
}

Il metodo precedente segue un tipico idioma .NET: il valore restituito indica se message è stato impostato sul valore trovato o, se non viene trovato alcun messaggio, sul valore predefinito. Se il metodo restituisce true, il valore di message non è Null; in caso contrario, il metodo imposta message su Null.

In un contesto abilitato nullable è possibile comunicare tale linguaggio usando l'attributo NotNullWhen. Quando si annotano i parametri per i tipi riferimento nullable, rendere message un oggetto string? e aggiungere un attributo:

bool TryGetMessage(string key, [NotNullWhen(true)] out string? message)
{
    if (_messageMap.ContainsKey(key))
        message = _messageMap[key];
    else
        message = null;
    return message is not null;
}

Nell'esempio precedente il valore di message è noto come non null quando TryGetMessage restituisce true. È consigliabile annotare metodi simili nella codebase nello stesso modo: gli argomenti possono essere uguali a null, e sono noti per non essere Null quando il metodo restituisce true.

Potrebbe essere necessario anche un attributo finale. A volte lo stato Null di un valore restituito dipende dallo stato Null di uno o più argomenti. Questi metodi restituiranno un valore non Null ogni volta che determinati argomenti non sono null. Per annotare correttamente questi metodi, usare l'attributo NotNullIfNotNull. Si consideri il modello seguente:

string GetTopLevelDomainFromFullUrl(string url)

Se l'argomento url non è Null, l'output non è null. Dopo aver abilitato i riferimenti nullable, è necessario aggiungere altre annotazioni se l'API può accettare un argomento Null. È possibile annotare il tipo restituito come illustrato nel codice seguente:

string? GetTopLevelDomainFromFullUrl(string? url)

Anche questo funziona, ma spesso costringe i chiamanti a implementare controlli null aggiuntivi. Il contratto è che il valore restituito sarà null solo quando l'argomento url è null. Per esprimere tale contratto, è necessario annotare questo metodo come illustrato nel codice seguente:

[return: NotNullIfNotNull(nameof(url))]
string? GetTopLevelDomainFromFullUrl(string? url)

Nell'esempio precedente viene usato l'operatore nameof per il parametro url. Questa funzionalità è disponibile in C# 11. Prima di C# 11, sarà necessario digitare il nome del parametro come stringa. Il valore restituito e l'argomento sono stati entrambi annotati con l'oggetto ? che indica che uno dei due può essere null. L'attributo chiarisce ulteriormente che il valore restituito non sarà Null quando l'argomento url non è null.

È possibile specificare delle postcondizioni condizionali usando questi attributi:

  • MaybeNullWhen: un argomento non nullable può essere Null quando il metodo restituisce il valore specificato bool.
  • NotNullWhen: un argomento nullable non sarà Null quando il metodo restituisce il valore specificato bool.
  • NotNullIfNotNull: un valore restituito non è Null se l'argomento per il parametro specificato non è Null.

Metodi helper: MemberNotNull e MemberNotNullWhen

Questi attributi specificano la finalità quando è stato eseguito il refactoring del codice comune dai costruttori nei metodi helper. Il compilatore C# analizza i costruttori e gli inizializzatori di campo per assicurarsi che tutti i campi di riferimento non nullable siano stati inizializzati prima che ogni costruttore restituisca. Tuttavia, il compilatore C# non tiene traccia delle assegnazioni di campo tramite tutti i metodi helper. Il compilatore genera un avviso CS8618 quando i campi non vengono inizializzati direttamente nel costruttore, ma piuttosto in un metodo helper. Aggiungere l'oggetto MemberNotNullAttribute a una dichiarazione di metodo e specificare i campi inizializzati in un valore non Null nel metodo. Ad esempio, si consideri l'esempio seguente:

public class Container
{
    private string _uniqueIdentifier; // must be initialized.
    private string? _optionalMessage;

    public Container()
    {
        Helper();
    }

    public Container(string message)
    {
        Helper();
        _optionalMessage = message;
    }

    [MemberNotNull(nameof(_uniqueIdentifier))]
    private void Helper()
    {
        _uniqueIdentifier = DateTime.Now.Ticks.ToString();
    }
}

È possibile specificare più nomi di campo come argomenti per il costruttore dell'attributo MemberNotNull.

L'oggetto MemberNotNullWhenAttribute ha un argomento bool. Si usa MemberNotNullWhen in situazioni in cui il metodo helper restituisce un oggetto bool che indica se il metodo helper ha inizializzato i campi.

Arrestare l'analisi nullable quando viene generato un metodo chiamato

Alcuni metodi, in genere helper eccezioni o altri metodi di utilità, escono sempre generando un'eccezione. In alternativa, un helper può generare un'eccezione in base al valore di un argomento booleano.

Nel primo caso, è possibile aggiungere l'attributo DoesNotReturnAttribute alla dichiarazione del metodo. L'analisi dello stato Null del compilatore non controlla alcun codice in un metodo che segue una chiamata a un metodo annotato con DoesNotReturn. Si consideri il metodo seguente:

[DoesNotReturn]
private void FailFast()
{
    throw new InvalidOperationException();
}

public void SetState(object containedField)
{
    if (containedField is null)
    {
        FailFast();
    }

    // containedField can't be null:
    _field = containedField;
}

Il compilatore non genera avvisi dopo la chiamata a FailFast.

Nel secondo caso, si aggiunge l'attributo System.Diagnostics.CodeAnalysis.DoesNotReturnIfAttribute a un parametro booleano del metodo. È possibile modificare l'esempio precedente come segue:

private void FailFastIf([DoesNotReturnIf(true)] bool isNull)
{
    if (isNull)
    {
        throw new InvalidOperationException();
    }
}

public void SetFieldState(object? containedField)
{
    FailFastIf(containedField == null);
    // No warning: containedField can't be null here:
    _field = containedField;
}

Quando il valore dell'argomento corrisponde al valore del costruttore DoesNotReturnIf, il compilatore non esegue alcuna analisi dello stato Null dopo tale metodo.

Riepilogo

L'aggiunta di tipi riferimento nullable fornisce un vocabolario iniziale per descrivere le aspettative delle API per le variabili che potrebbero essere null. Gli attributi forniscono un vocabolario più completo per descrivere lo stato Null delle variabili come precondizioni e postcondizioni. Questi attributi descrivono in modo più chiaro le aspettative e offrono un'esperienza migliore per gli sviluppatori che usano le API.

Quando si aggiornano le librerie per un contesto nullable, aggiungere questi attributi per guidare gli utenti delle API all'utilizzo corretto. Questi attributi consentono di descrivere completamente lo stato Null degli argomenti e i valori restituiti.

  • AllowNull: un campo, un parametro o una proprietà non nullable può essere Null.
  • DisallowNull: un campo, un parametro o una proprietà nullable non deve mai essere Null.
  • MaybeNull: un campo, un parametro, una proprietà o un valore restituito non nullable può essere Null.
  • NotNull: un campo nullable, un parametro, una proprietà o un valore restituito non sarà mai Null.
  • MaybeNullWhen: un argomento non nullable può essere Null quando il metodo restituisce il valore specificato bool.
  • NotNullWhen: un argomento nullable non sarà Null quando il metodo restituisce il valore specificato bool.
  • NotNullIfNotNull: un parametro, una proprietà o un valore restituito non è Null se l'argomento per il parametro specificato non è Null.
  • DoesNotReturn: un metodo o una proprietà non restituisce mai. In altre parole, genera sempre un'eccezione.
  • DoesNotReturnIf: questo metodo o proprietà non restituisce mai se il parametro bool associato ha il valore specificato.