Attributs pour l’analyse statique à état Null interprété par le compilateur C#

Dans un contexte activé nullable, 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érencer une valeur null, en lisant un System.NullReferenceException. Ces attributs fournissent au compilateur des informations sémantiques sur l’état null des arguments, les valeurs de retour et les membres d’objet en fonction de l’état des arguments et des valeurs de retour. Le compilateur fournit des avertissements plus précis lorsque vos API ont été correctement annotées avec ces informations sémantiques.

Cet article fournit une brève description de chacun des attributs de type référence nullable et de leur utilisation.

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 d’oubli 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 familier Try* dans .NET. Il existe deux paramètres de référence pour cette API : le key et le message. Cette API a les règles suivantes relatives à l’état Null de ces paramètres :

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

La règle pour key laquelle peut être exprimée succinctement : key doit être un type référence non Nullable. Le message paramètre est plus complexe. Elle 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 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 de votre API. Lors de la compilation du code dans un contexte activé nullable, le compilateur avertit les appelants lorsqu’ils violent ces règles. Ces attributs n’activent pas plus de vérifications sur votre implémentation.

Attribut Category Signification
AllowNull Precondition Un paramètre, un champ ou une propriété non nullable peut avoir la valeur Null.
DisallowNull Precondition Un paramètre nullable, un champ ou une propriété ne doit jamais être null.
Peut-êtreNull 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 ne sera jamais null.
Peut-êtreNullWhen Postcondition conditionnelle Un argument non nullable peut être null lorsque la méthode retourne la valeur spécifiée bool .
NotNullWhen Postcondition conditionnelle Un argument nullable ne sera pas null lorsque la méthode retourne la valeur spécifiée bool .
NotNullIfNotNull Postcondition 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é ne sera 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é ne sera 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 cette propriété ne retourne jamais si le paramètre associé bool 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

Considérez une propriété en lecture/écriture qui ne retourne null jamais parce qu’elle a une valeur par défaut raisonnable. Les appelants null passent à l’accesseur set lors de la définition de cette valeur par défaut. Par exemple, considérez 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;

Lorsque vous compilez le code précédent dans un contexte d’oubli nullable, tout va bien. Une fois que vous avez activé les types référence nullable, la ScreenName propriété devient une référence non nullable. C’est correct pour l’accesseur get : il ne retourne nulljamais . 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é pour null générer 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 décrits dans cet article. L’attribut est appliqué à la propriété, et non à l’accesseur set . L’attribut AllowNull spécifie les 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’il ne doit pas être null, donc vous souhaitez un type référence non Nullable.
  2. Il existe des scénarios pour qu’un appelant passe null comme argument, bien qu’il ne s’agit pas de l’utilisation la plus courante.

Le plus souvent, vous aurez besoin de cet attribut pour les propriétés, ou inout, et ref les arguments. L’attribut AllowNull est le meilleur choix lorsqu’une variable est généralement non null, mais que vous devez autoriser null comme condition préalable.

Contraste avec les scénarios d’utilisation DisallowNull: vous utilisez cet attribut pour spécifier qu’un argument d’un type référence nullable ne doit pas être null. Considérez une propriété où null se trouve la valeur par défaut, mais les clients peuvent uniquement le définir sur une valeur non null. Considérez 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 le ReviewComment peut être null, mais ne peut pas être défini nullsur . Une fois que ce code est conscient de la valeur Null, vous pouvez exprimer ce concept plus clairement aux appelants à l’aide des System.Diagnostics.CodeAnalysis.DisallowNullAttributeéléments suivants :

[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 pourrait être null, les appelants ne doivent pas le définir nullexplicitement sur . L’attribut DisallowNull spécifie également une condition préalable, il n’affecte pas l’accesseur get . Vous utilisez l’attribut DisallowNull lorsque vous observez les caractéristiques suivantes :

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

Ces situations sont courantes dans le code qui était initialement nul. 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. Celles-ci fournissent plus de détails sur les caractéristiques de votre API. Ces informations supplémentaires aident les appelants à utiliser correctement votre API. N’oubliez pas que vous spécifiez les 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.

Postconditions : 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é. L’enregistrement null indique clairement que l’enregistrement n’a pas été trouvé. Dans cet exemple, vous allez probablement modifier le type de retour de Customer .Customer? La déclaration de la valeur de retour en tant que type de 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é générique que 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 lorsque l’élément recherché n’est pas trouvé. Vous pouvez clarifier que la méthode retourne null lorsqu’un élément n’est pas trouvé en ajoutant l’annotation MaybeNull à la méthode retournée :

[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, Tvous pouvez exprimer qu’elle ne retourne null jamais à l’aide de 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 de référence nullable. La méthode suivante est une méthode d’assistance qui lève 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 de référence Null, vous souhaitez vous assurer que le code précédent se compile sans avertissement. Lorsque la méthode est retournée, le value paramètre est garanti qu’il n’est pas null. Toutefois, il est acceptable d’appeler ThrowWhenNull avec une référence Null. Vous pouvez créer value un type de référence nullable et ajouter la NotNull condition post-condition à 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 passer une variable avec la null valeur, mais l’argument n’est jamais null si la méthode retourne sans lever d’exception.

Vous spécifiez des postconditions inconditionnelles à l’aide des attributs suivants :

  • Peut-êtreNull : une valeur de retour non nullable peut être null.
  • NotNull : une valeur de retour nullable ne sera jamais null.

Conditions ultérieures conditionnelles : NotNullWhen, MaybeNullWhenet NotNullIfNotNull

Vous êtes probablement familiarisé avec la string méthode String.IsNullOrEmpty(String). Cette méthode retourne true quand l’argument est null ou une chaîne vide. Il s’agit d’une forme de vérification null : les appelants n’ont pas besoin de vérifier l’argument si la méthode retourne false. Pour faire en sorte qu’une méthode comme cette valeur nullable soit prise en compte, vous devez définir l’argument sur un type de 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 n’a false pas besoin de vérifications Null. L’ajout de l’attribut informe l’analyse statique du compilateur qui IsNullOrEmpty effectue la vérification 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 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 vérification null personnalisées, et vous devez ajouter les annotations vous-même. Lorsque vous ajoutez l’attribut, l’analyse statique du compilateur sait quand la variable testée a été vérifiée null.

Une autre utilisation pour ces attributs est le Try* modèle. Les postconditions pour ref et out les arguments sont communiquées par le biais de la valeur de retour. Considérez cette méthode indiquée précédemment (dans un contexte désactivé nullable) :

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 idiom .NET classique : la valeur de retour indique si message elle a été définie sur la valeur trouvé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 la valeur Null.

Dans un contexte activé par null, vous pouvez communiquer cette idiom à l’aide de l’attribut NotNullWhen . Lorsque vous annotez des paramètres pour les types de référence nullables, créez message un string? attribut 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 d’est message connue pour ne pas être null quand TryGetMessage elle est retournée true. Vous devez annoter les méthodes similaires de votre codebase de la même façon : les arguments peuvent être égaux nullet sont connus pour ne pas être null lorsque la méthode retourne true.

Vous avez peut-être besoin d’un attribut final. Parfois, l’état null d’une valeur de retour dépend de l’état null d’un ou 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 . Tenez compte de 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 vérifications supplémentaires null . Le contrat est que la valeur de retour ne serait null que lorsque l’argument url est null. Pour exprimer ce contrat, vous annotez 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 en C# 11. Avant C# 11, vous devez taper le nom du paramètre en tant que chaîne. La valeur de retour et l’argument ont tous les deux été annotés avec l’indication ? que l’un ou l’autre pourrait être null. L’attribut précise davantage que la valeur de retour ne sera pas null lorsque l’argument url n’est pas null.

Vous spécifiez des postconditions 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 ne sera 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 avez refactorisé le code commun des constructeurs dans des méthodes d’assistance. Le compilateur C# analyse les constructeurs et les initialiseurs de champs pour vous assurer que tous les champs de référence non nullables ont été initialisés avant que chaque constructeur ne retourne. Toutefois, le compilateur C# ne suit pas les affectations de champs par le biais de toutes les méthodes d’assistance. Le compilateur émet un avertissement CS8618 lorsque les champs ne sont pas initialisés directement dans le constructeur, mais plutôt dans une méthode d’assistance. Vous ajoutez la MemberNotNullAttribute valeur à une déclaration de méthode et spécifiez les champs initialisés à 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 en tant qu’arguments pour le constructeur d’attribut MemberNotNull .

Il MemberNotNullWhenAttribute a un bool argument. Vous utilisez MemberNotNullWhen dans des situations où votre méthode d’assistance retourne un bool indicateur indiquant si vos champs initialisés par la méthode d’assistance.

Arrêter l’analyse nullable lorsque la méthode appelée lève

Certaines méthodes, généralement des assistants d’exception ou d’autres méthodes utilitaires, quittent toujours en lève une exception. Ou, un assistance peut lever 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 méthode. L’analyse de l’é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 ne é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 DoesNotReturnIf valeur du constructeur, le compilateur n’effectue aucune analyse d’état null après cette méthode.

Résumé

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

Lorsque vous mettez à jour les bibliothèques pour un contexte nullable, ajoutez ces attributs pour guider les utilisateurs de vos API à 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 nullable, un paramètre ou une propriété 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 ne sera 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 ne sera 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. En d’autres termes, elle lève toujours une exception.
  • DoesNotReturnIf : cette méthode ou cette propriété ne retourne jamais si le paramètre associé bool a la valeur spécifiée.