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.
  • maybe-null : l’analyse statique ne peut pas déterminer qu’une variable a une valeur non-null.

Ces états permettent au compilateur de fournir des avertissements lorsque vous pouvez déréférencer une valeur null, en levant une exception System.NullReferenceException. Ces attributs fournissent au compilateur des informations sémantiques sur l’état null des arguments, des valeurs de retour et des membres d’objet en fonction de l’état des arguments et des valeurs de retour. Le compilateur fournit des avertissements plus précis quand vos API ont été 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 dispose de l’API suivante pour récupérer 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 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 ci-dessous, décrit l’état null de l’argument utilisé pour le paramètre message.

Notes

L’ajout de ces attributs donne au compilateur plus d’informations sur les règles pour votre API. Quand le code d’appel est compilé dans un contexte prenant en compte les types nullables, le compilateur avertit les appelants quand ils enfreignent 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, un champ, une propriété ou une valeur de retour non-nullable peut être null.
NotNull Postcondition Un paramètre, un champ, une propriété ou une valeur de retour nullable ne sera jamais null.
MaybeNullWhen Post-condition conditionnelle Un argument non-nullable peut être null quand la méthode retourne la valeur bool spécifiée.
NotNullWhen Post-condition conditionnelle Un argument nullable n’est pas null quand la méthode retourne la valeur bool spécifiée.
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 listé n’est pas null quand la méthode est retournée.
MemberNotNullWhen Méthodes d’assistance de méthode et de propriété Le membre listé n’est pas null quand la méthode retourne la valeur bool spécifiée.
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. Ceci est correct pour l’accesseur get : il ne retourne jamais null. 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 directive using pour System.Diagnostics.CodeAnalysis afin d’utiliser les attribut décrits 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 aurez besoin de cet attribut pour les propriétés, ou les arguments in, out et ref. 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 nullable, l’accesseur ReviewCommentget peut retourner 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 seulement après la fin d’un travail asynchrone.

Les attributs AllowNull et DisallowNull vous permettent de spécifier que les conditions préalables sur les variables peuvent ne pas correspondre aux annotations nullables sur ces variables. Celles-ci 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 quand 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 les raisons décrites sous 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 en fait être null. Il informe également le compilateur que la méthode peut retourner une expression null même si le type est non-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 :

  • MaybeNull : une valeur de retour non-nullable peut être null.
  • NotNull : une valeur de retour nullable ne sera 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.

Notes

L’exemple précédent est valide uniquement pour C# 11 et versions ultérieures. À partir de C# 11, l’nameofexpression peut référencer des noms de paramètre et de paramètre de type lorsqu’elle est utilisée dans un attribut appliqué à une méthode. Dans C# 10 et versions antérieures, vous devez utiliser un littéral de chaîne au lieu de l’expression nameof.

La méthode String.IsNullOrEmpty(String) sera annotée comme indiqué ci-dessus pour .NET Core 3.0. 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 contrôle de valeur null personnalisées et vous devrez ajouter vous-même les annotations. Lorsque vous ajoutez l’attribut, l’analyse statique du compilateur sait quand la variable testée a fait l’objet d’un contrôle de valeur 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 un idiome .NET standard : la valeur de retour indique si message a été défini sur la valeur trouvée ou, si aucun message n’est trouvé, sur 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.

Vous pouvez également avoir besoin d’un attribut final. 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 de valeur null supplémentaires. 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. Cette fonctionnalité est disponible dans C# 11. Avant C# 11, vous devez taper le nom du paramètre sous forme de chaîne. 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 clarifie également que la valeur de retour ne sera pas null quand l’argument url ne sera pas null.

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

  • MaybeNullWhen : un argument non-nullable peut être null quand la méthode retourne la valeur bool spécifiée.
  • NotNullWhen : un argument nullable ne sera pas null quand la méthode retournera la valeur bool spécifiée.
  • 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 avez refactorisé du code commun à partir de constructeurs en 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 ont été initialisés avant le retour de chaque constructeur. 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 assistants d’exception ou d’autres méthodes utilitaires, s’arrêtent toujours en levant une exception. Ou bien, une méthode d’assistance peut lever une exception basée sur 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.
  • MaybeNull : un champ, un paramètre, une propriété ou une valeur de retour non-nullable peut être null.
  • NotNull : un champ, un paramètre, une propriété ou une valeur de retour nullable ne sera jamais null.
  • MaybeNullWhen : un argument non-nullable peut être null quand la méthode retourne la valeur bool spécifiée.
  • NotNullWhen : un argument nullable ne sera pas null quand la méthode retournera la valeur bool spécifiée.
  • 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.