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 pourkey
. - Les appelants peuvent transmettre une variable dont la valeur est
null
comme argument pourmessage
. - Si la méthode
TryGetMessage
retournetrue
, la valeur demessage
n’est pas null. Si la valeur de retour estfalse
, la valeur demessage
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 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 :
- 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. - 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 pouvant accepter la valeur Null, l’accesseur ReviewComment
get
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 :
- La variable peut être
null
dans des scénarios principaux, souvent lors de sa première instanciation. - 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 string
String.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 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.