Partager via


Attributs pour l’analyse statique d’état null interprétés par le compilateur C#

Dans un contexte prenant en compte les types nullables, le compilateur effectue une analyse statique du code pour déterminer l’état null de toutes les variables de type référence :

  • not-null : l’analyse statique détermine qu’une variable a une valeur non-null.
  • peut-être null : l’analyse statique ne peut pas déterminer qu’une variable est affectée à une valeur non null.

Ces états permettent au compilateur de fournir des avertissements lorsque vous pouvez déréférer une valeur Null, en lisant un System.NullReferenceException. Ces attributs fournissent au compilateur des informations sémantiques sur l’état null des arguments, des valeurs de retour et des membres de l’objet. Les attributs clarifient l’état des arguments et des valeurs de retour. Le compilateur fournit des avertissements plus précis lorsque vos API sont correctement annotées avec ces informations sémantiques.

Cet article fournit une brève description de chaque attribut de type référence nullable et de la manière de l’utiliser.

Commençons avec un exemple. Imaginez que votre bibliothèque possède l’API suivante qui récupère une chaîne de ressource. Cette méthode a été compilée à l’origine dans un contexte inconscient de nullable :

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

L’exemple précédent suit le modèle Try* familier dans .NET. Il existe deux paramètres de référence pour cette API : key et message. Cette API a les règles suivantes relatives à l’état null de ces paramètres :

  • Les appelants ne doivent pas transmettre null comme argument pour key.
  • Les appelants peuvent transmettre une variable dont la valeur est null comme argument pour message.
  • Si la méthode TryGetMessage retourne true, la valeur de message n’est pas null. Si la valeur de retour est false, la valeur de message la valeur est Null.

La règle pour key peut être exprimée succinctement : key doit être un type référence non-nullable. Le paramètre message est plus complexe. Il autorise une variable qui est null comme argument, mais garantit, en cas de réussite, que l’argument out n’est pas null. Pour ces scénarios, vous avez besoin d’un vocabulaire plus riche pour décrire les attentes. L’attribut NotNullWhen décrit l’état null de l’argument utilisé pour le message paramètre.

Notes

L’ajout de ces attributs donne au compilateur plus d’informations sur les règles pour votre API. Lorsque vous appelez du code est compilé dans un contexte activé nullable, le compilateur avertit les appelants lorsqu’ils violent ces règles. Ces attributs n’activent pas davantage de vérifications sur votre implémentation.

Attribut Category Signification
AllowNull Precondition Un paramètre, un champ ou une propriété non nullable peut être null.
DisallowNull Precondition Un paramètre, un champ ou une propriété nullable ne doit jamais être null.
MaybeNull Postcondition Un paramètre non nullable, un champ, une propriété ou une valeur de retour peut être null.
NotNull Postcondition Un paramètre nullable, un champ, une propriété ou une valeur de retour n’est jamais null.
MaybeNullWhen Post-condition conditionnelle Un argument non nullable peut être null lorsque la méthode retourne la valeur spécifiée bool .
NotNullWhen Post-condition conditionnelle Un argument nullable n’est pas null lorsque la méthode retourne la valeur spécifiée bool .
NotNullIfNotNull Post-condition conditionnelle Une valeur de retour, une propriété ou un argument n’est pas null si l’argument du paramètre spécifié n’est pas null.
MemberNotNull Méthodes d’assistance de méthode et de propriété Le membre répertorié n’est pas null lorsque la méthode est retournée.
MemberNotNullWhen Méthodes d’assistance de méthode et de propriété Le membre répertorié n’est pas null lorsque la méthode retourne la valeur spécifiée bool .
DoesNotReturn Code inaccessible Une méthode ou une propriété ne retourne jamais. En d’autres termes, elle lève toujours une exception.
DoesNotReturnIf Code inaccessible Cette méthode ou propriété ne retourne jamais si le paramètre bool associé a la valeur spécifiée.

Les descriptions précédentes sont une référence rapide à ce que fait chaque attribut. Les sections suivantes décrivent plus en détail le comportement et la signification de ces attributs.

Conditions préalables : AllowNull et DisallowNull

Prenons l’exemple d’une propriété en lecture/écriture qui ne retourne jamais null, car elle a une valeur par défaut raisonnable. Les appelants transmettent null à l’accesseur set lorsqu’ils le définissent sur cette valeur par défaut. Par exemple, prenons l’exemple d’un système de messagerie qui demande un nom d’écran dans une salle de conversation. Si aucun n’est fourni, le système génère un nom aléatoire :

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

Quand vous compilez le code précédent dans un contexte inconscient de nullable, tout se passe correctement. Une fois que vous activez les types référence nullables, la propriété ScreenName devient une référence non-nullable. Les appelants n’ont pas besoin de vérifier la propriété retournée pour null. Mais maintenant, la définition de la propriété sur null génère un avertissement. Pour prendre en charge ce type de code, vous ajoutez l’attribut System.Diagnostics.CodeAnalysis.AllowNullAttribute à la propriété, comme indiqué dans le code suivant :

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

Vous devrez peut-être ajouter une using directive pour System.Diagnostics.CodeAnalysis utiliser cet attribut et d’autres attributs abordés dans cet article. L’attribut est appliqué à la propriété, et non pas à l’accesseur set. L’attribut AllowNull spécifie des conditions préalables et s’applique uniquement aux arguments. L’accesseur get a une valeur de retour, mais aucun paramètre. Par conséquent, l’attribut AllowNull s’applique uniquement à l’accesseur set.

L’exemple précédent montre ce qu’il faut rechercher lors de l’ajout de l’attribut AllowNull sur un argument :

  1. Le contrat général pour cette variable est qu’elle ne doit pas être null, vous souhaitez donc un type référence non-nullable.
  2. Il existe des scénarios pour qu’un appelant transmette null comme argument, bien qu’il ne s’agisse pas de l’utilisation la plus courante.

Le plus souvent, vous avez besoin de cet attribut pour les propriétés, ou in, outet ref les arguments. L’attribut AllowNull est le meilleur choix quand une variable n’est généralement pas null, mais vous devez autoriser null comme condition préalable.

Comparez cela avec les scénarios d’utilisation de DisallowNull : vous utilisez cet attribut pour spécifier qu’un argument d’un type référence nullable ne doit pas être null. Prenons l’exemple d’une propriété où null est la valeur par défaut, mais les clients peuvent uniquement la définir sur une valeur non-null. Prenez le code suivant :

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

Le code précédent est la meilleure façon d’exprimer votre conception que ReviewComment peut être null, mais ne peut pas être défini sur null. Une fois que ce code est conscient de nullable, vous pouvez exprimer ce concept plus clairement aux appelants à l’aide de System.Diagnostics.CodeAnalysis.DisallowNullAttribute :

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

Dans un contexte pouvant accepter la valeur Null, l’accesseur ReviewCommentget retournerait la valeur par défaut de null. Le compilateur avertit qu’il doit être vérifié avant l’accès. En outre, il avertit les appelants que, même s’il peut s’agir de null, les appelants ne doivent pas le définir explicitement sur null. L’attribut DisallowNull spécifie également une condition préalable, elle n’affecte pas l’accesseur get. Vous utilisez l’attribut DisallowNull quand vous observez ces caractéristiques sur :

  1. La variable peut être null dans des scénarios principaux, souvent lors de sa première instanciation.
  2. La variable ne doit pas être explicitement définie sur null.

Ces situations sont courantes dans le code qui était à l’origine inconscient de null. Il se peut que les propriétés d’objet soient définies dans deux opérations d’initialisation distinctes. Il se peut que certaines propriétés soient définies uniquement après la fin d’un travail asynchrone.

Les AllowNull attributs et DisallowNull les attributs vous permettent de spécifier que les conditions préalables sur les variables peuvent ne pas correspondre aux annotations nullables sur ces variables. Ces annotations fournissent plus de détails sur les caractéristiques de votre API. Ces informations supplémentaires aident les appelants à utiliser votre API correctement. N’oubliez pas que vous spécifiez des conditions préalables à l’aide des attributs suivants :

  • AllowNull : un argument non nullable peut être null.
  • DisallowNull : un argument nullable ne doit jamais être null.

Post-conditions : MaybeNull et NotNull

Supposons que vous ayez une méthode avec la signature suivante :

public Customer FindCustomer(string lastName, string firstName)

Vous avez probablement écrit une méthode comme celle-ci pour retourner null lorsque le nom recherché n’a pas été trouvé. null indique clairement que l’enregistrement n’a pas été trouvé. Dans cet exemple, vous changeriez probablement le type de retour en remplaçant Customer par Customer?. La déclaration de la valeur de retour en tant que type référence nullable spécifie clairement l’intention de cette API :

public Customer? FindCustomer(string lastName, string firstName)

Pour des raisons couvertes par la nullabilité des génériques , cette technique peut ne pas produire l’analyse statique qui correspond à votre API. Vous pouvez avoir une méthode générique qui suit un modèle similaire :

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

La méthode retourne null quand l’élément recherché est introuvable. Vous pouvez préciser que la méthode retourne null quand un élément est introuvable en ajoutant l’annotation MaybeNull au retour de la méthode :

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

Le code précédent informe les appelants que la valeur de retour peut réellement être null. Il informe également le compilateur que la méthode peut retourner une null expression même si le type n’est pas nullable. Lorsque vous avez une méthode générique qui retourne une instance de son paramètre de type, T, vous pouvez exprimer qu’elle ne retourne jamais null en utilisant l’attribut NotNull.

Vous pouvez également spécifier qu'une valeur de retour ou un argument n'est pas null, même si le type est un type référence nullable. La méthode suivante est une méthode d'aide qui lance la procédure si son premier argument est null :

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

Vous pouvez appeler cette routine comme suit :

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

    Console.WriteLine(message.Length);
}

Après avoir activé les types référence null, vous souhaitez vous assurer que le code précédent se compilera sans avertissements. Lorsque la méthode retourne son résultat, il est garanti que le paramètre value n’est pas null. Toutefois, il est acceptable d’appeler ThrowWhenNull avec une référence null. Vous pouvez faire de value un type référence nullable et ajouter la post-condition NotNull à la déclaration de paramètre :

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

Le code précédent exprime clairement le contrat existant : les appelants peuvent transmettre une variable avec la valeur null, mais il est garanti que l’argument n’est jamais null si la méthode retourne son résultat sans lever d’exception.

Vous spécifiez des post-conditions inconditionnelles à l’aide des attributs suivants :

  • Peut-êtreNull : une valeur de retour non nullable peut être null.
  • NotNull : une valeur de retour nullable n’est jamais null.

Post-conditions conditionnelles : NotNullWhen, MaybeNullWhen et NotNullIfNotNull

Vous connaissez probablement la méthode stringString.IsNullOrEmpty(String). Cette méthode retourne true quand l’argument est null ou une chaîne vide. Ceci est une forme de contrôle de valeur null : les appelants n’ont pas besoin d’effectuer un contrôle de valeur null de l’argument si la méthode retourne false. Pour faire qu’une méthode comme celle-ci prenne en compte la valeur nullable, vous devez définir l’argument sur un type référence nullable et ajouter l’attribut NotNullWhen :

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

Cela informe le compilateur que tout code où la valeur de retour est false n’a pas besoin de contrôles de valeur null. L’ajout de l’attribut informe l’analyse statique du compilateur que IsNullOrEmpty effectue le contrôle de valeur null nécessaire : quand il retourne false, l’argument n’est pas null.

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

La String.IsNullOrEmpty(String) méthode est illustrée dans l’exemple précédent. Vous pouvez avoir des méthodes similaires dans votre codebase qui vérifient l’état des objets pour les valeurs Null. Le compilateur ne reconnaît pas les méthodes de vérification null personnalisées et vous devez ajouter vous-même les annotations. Lorsque vous ajoutez l’attribut, l’analyse statique du compilateur sait quand la variable testée est vérifiée par null.

Le modèle Try* est une autre utilisation pour ces attributs. Les post-conditions pour les arguments ref et out sont communiquées par le biais de la valeur de retour. Considérez cette méthode présentée précédemment (dans un contexte ne prenant pas en compte les types nullables) :

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

La méthode précédente suit une idiome .NET classique : la valeur de retour indique si message elle a été définie sur la valeur recherchée ou, si aucun message n’est trouvé, à la valeur par défaut. Si la méthode retourne true, la valeur de message n’est pas null ; sinon, la méthode définit message sur null.

Dans un contexte prenant en compte les types nullables, vous pouvez communiquer cet idiome à l’aide de l’attribut NotNullWhen. Lorsque vous annotez des paramètres pour les types référence nullables, faites de message un string? et ajoutez un attribut :

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

Dans l’exemple précédent, la valeur de message est connue pour n’être pas null quand TryGetMessage retourne true. Vous devez annoter des méthodes similaires dans votre codebase de la même façon : les arguments peuvent être égaux à null, et sont connus pour ne pas être null quand la méthode retourne true.

Il existe un attribut final dont vous aurez peut-être besoin. Parfois, l’état null d’une valeur de retour dépend de l’état null d’un ou de plusieurs arguments. Ces méthodes retournent une valeur non Null chaque fois que certains arguments ne sont pas null. Pour annoter correctement ces méthodes, vous utilisez l’attribut NotNullIfNotNull. Considérez la méthode suivante :

string GetTopLevelDomainFromFullUrl(string url)

Si l’argument url n’est pas null, la sortie n’est pas null. Une fois les références nullables activées, vous devez ajouter d’autres annotations si votre API peut accepter un argument Null. Vous pouvez annoter le type de retour comme indiqué dans le code suivant :

string? GetTopLevelDomainFromFullUrl(string? url)

Cela fonctionne également, mais force souvent les appelants à implémenter des contrôles supplémentaires null . Le contrat stipule que la valeur de retour est null uniquement quand l’argument url est null. Pour exprimer ce contrat, vous devez annoter cette méthode comme indiqué dans le code suivant :

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

L’exemple précédent utilise l’opérateur nameof pour le paramètre url. La valeur de retour et l’argument ont tous les deux été annotés avec le ? indiquant que l’un ou l’autre peut être null. L’attribut précise davantage que la valeur de retour n’est pas null lorsque l’argument url n’est pas null.

Vous spécifiez des post-conditions conditionnelles à l’aide de ces attributs :

  • Peut-êtreNullWhen : un argument non nullable peut être null lorsque la méthode retourne la valeur spécifiée bool .
  • NotNullWhen : un argument nullable n’est pas null lorsque la méthode retourne la valeur spécifiée bool .
  • NotNullIfNotNull : une valeur de retour n’est pas null si l’argument du paramètre spécifié n’est pas null.

Méthodes d'assistance : MemberNotNull et MemberNotNullWhen

Ces attributs spécifient votre intention lorsque vous refactorisez le code commun des constructeurs dans des méthodes d’assistance. Le compilateur C# analyse les constructeurs et les initialiseurs de champs pour s’assurer que tous les champs de référence non nullables sont initialisés avant que chaque constructeur ne retourne. Toutefois, le compilateur C# ne suit pas les affectations de champs via toutes les méthodes d’assistance. Le compilateur émet un avertissement CS8618 quand des champs ne sont pas initialisés directement dans le constructeur, mais plutôt dans une méthode d’assistance. Vous ajoutez l’attribut MemberNotNullAttribute à une déclaration de méthode et spécifiez les champs qui sont initialisés sur une valeur non-null dans la méthode. Considérez l’exemple suivant :

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();
    }
}

Vous pouvez spécifier plusieurs noms de champs comme arguments pour le constructeur d’attribut MemberNotNull.

L’attribut MemberNotNullWhenAttribute a un argument bool. Vous utilisez MemberNotNullWhen dans les situations où votre méthode d’assistance retourne un bool indiquant si votre méthode d’assistance a initialisé des champs.

Arrêter l’analyse nullable quand la méthode appelée lève une exception

Certaines méthodes, généralement des helpers d’exception ou d’autres méthodes utilitaires, quittent toujours en lèvent une exception. Ou, un assistance lève une exception en fonction de la valeur d’un argument booléen.

Dans le premier cas, vous pouvez ajouter l’attribut DoesNotReturnAttribute à la déclaration de la méthode. L’analyse d’état null du compilateur ne vérifie aucun code dans une méthode qui suit un appel à une méthode annotée avec DoesNotReturn. Prenons la méthode suivante :

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

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

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

Le compilateur n’émet aucun avertissement après l’appel à FailFast.

Dans le deuxième cas, vous ajoutez l’attribut System.Diagnostics.CodeAnalysis.DoesNotReturnIfAttribute à un paramètre booléen de la méthode. Vous pouvez modifier l’exemple précédent comme suit :

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;
}

Lorsque la valeur de l’argument correspond à la valeur du constructeur DoesNotReturnIf, le compilateur n’effectue aucune analyse d’état null après cette méthode.

Résumé

L’ajout de types référence nullables fournit un vocabulaire initial pour décrire vos attentes d’API pour les variables qui peuvent être null. Les attributs fournissent un vocabulaire plus riche pour décrire l’état null des variables en tant que conditions préalables et post-conditions. Ces attributs décrivent plus clairement vos attentes et fournissent une meilleure expérience aux développeurs qui utilisent vos API.

Lorsque vous mettez à jour les bibliothèques pour un contexte nullable, ajoutez ces attributs pour guider les utilisateurs de vos API vers l’utilisation correcte. Ces attributs vous aident à décrire entièrement l’état null des arguments et des valeurs de retour.

  • AllowNull : un champ, un paramètre ou une propriété non nullable peut être null.
  • DisallowNull : un champ, un paramètre ou une propriété nullable ne doit jamais être null.
  • Peut-êtreNull : un champ, un paramètre, une propriété ou une valeur de retour non null peut être null.
  • NotNull : un champ nullable, un paramètre, une propriété ou une valeur de retour n’est jamais null.
  • Peut-êtreNullWhen : un argument non nullable peut être null lorsque la méthode retourne la valeur spécifiée bool .
  • NotNullWhen : un argument nullable n’est pas null lorsque la méthode retourne la valeur spécifiée bool .
  • NotNullIfNotNull : un paramètre, une propriété ou une valeur de retour n’est pas null si l’argument du paramètre spécifié n’est pas null.
  • DoesNotReturn : une méthode ou une propriété ne retourne jamais son résultat. En d’autres termes, elle lève toujours une exception.
  • DoesNotReturnIf : cette méthode ou propriété ne retourne jamais son résultat si le paramètre bool associé a la valeur spécifiée.