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 pourkey
. - Les appelants peuvent passer une variable dont la valeur est
null
l’argument pourmessage
. - Si la
TryGetMessage
méthode retournetrue
, la valeur demessage
n’est pas null. Si la valeur de retour estfalse,
la valeur nullmessage
.
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 null
jamais . 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 :
- 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. - 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 in
out
, 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 null
sur . 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 ReviewComment
get
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 null
explicitement 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 :
- La variable peut se trouver
null
dans des scénarios de base, souvent lors de la première instanciation. - 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, T
vous 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
, MaybeNullWhen
et 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 null
et 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.