Types références Nullables

Dans un contexte inconscient nullable, tous les types de référence étaient nullables. Les types de référence nullables font référence à un groupe de fonctionnalités activées dans un contexte prenant en charge les valeurs nullables qui réduisent la probabilité que votre code provoque la levée System.NullReferenceExceptiondu runtime . Les types de référence nullables incluent trois fonctionnalités qui vous permettent d’éviter ces exceptions, notamment la possibilité de marquer explicitement un type de référence comme nullable :

  • Amélioration de l’analyse de flux statique qui détermine si une variable peut être null avant de la déréférencer.
  • Attributs qui annotent les API afin que l’analyse de flux détermine l’état null.
  • Annotations de variables que les développeurs utilisent pour déclarer explicitement l’état null prévu pour une variable.

L’analyse de l’état Null et les annotations variables sont désactivées par défaut pour les projets existants, ce qui signifie que tous les types de référence continuent d’être nullables. À compter de .NET 6, ils sont activés par défaut pour les nouveaux projets. Pour plus d’informations sur l’activation de ces fonctionnalités en déclarant un contexte d’annotation nullable, consultez Contextes nullables.

Le reste de cet article décrit comment ces trois zones de fonctionnalités fonctionnent pour générer des avertissements lorsque votre code peut déréférencer une null valeur. Le déréférencement d’une variable signifie accéder à l’un de ses membres à l’aide de l’opérateur . (point), comme illustré dans l’exemple suivant :

string message = "Hello, World!";
int length = message.Length; // dereferencing "message"

Lorsque vous déréférencez une variable dont la valeur est null, le runtime lève un System.NullReferenceException.

Vous pouvez également explorer ces concepts dans notre module Learn sur la sécurité nullable en C#.

Analyse de l’état Null

L’analyse de l’état Null suit l’état null des références. Cette analyse statique émet des avertissements lorsque votre code peut déréférencer null. Vous pouvez traiter ces avertissements pour réduire les incidences lorsque le runtime lève un System.NullReferenceException. Le compilateur utilise l’analyse statique pour déterminer l’état null d’une variable. Une variable n’est pas null ou peut-être-null. Le compilateur détermine qu’une variable n’est pas null de deux manières :

  1. Une valeur connue pour n’être pas null a été attribuée à la variable.
  2. La variable a été vérifiée par rapport null à et n’a pas été modifiée depuis cette vérification.

Toute variable que le compilateur n’a pas déterminée comme non null est considérée comme peut-être-null. L’analyse fournit des avertissements dans les situations où vous pouvez déréférencer accidentellement une null valeur. Le compilateur génère des avertissements basés sur l’état null.

  • Lorsqu’une variable n’est pas null, cette variable peut être déréférencée en toute sécurité.
  • Lorsqu’une variable est peut-être null, cette variable doit être vérifiée pour s’assurer qu’elle ne l’est pas null avant de la déréférencer.

Prenons l’exemple suivant :

string message = null;

// warning: dereference null.
Console.WriteLine($"The length of the message is {message.Length}");

var originalMessage = message;
message = "Hello, World!";

// No warning. Analysis determined "message" is not null.
Console.WriteLine($"The length of the message is {message.Length}");

// warning!
Console.WriteLine(originalMessage.Length);

Dans l’exemple précédent, le compilateur détermine que message la valeur est peut-être null lorsque le premier message est imprimé. Il n’y a pas d’avertissement pour le deuxième message. La dernière ligne de code génère un avertissement, car originalMessage peut être null. L’exemple suivant montre une utilisation plus pratique pour parcourir une arborescence de nœuds jusqu’à la racine, en traitant chaque nœud pendant la traversée :

void FindRoot(Node node, Action<Node> processNode)
{
    for (var current = node; current != null; current = current.Parent)
    {
        processNode(current);
    }
}

Le code précédent ne génère aucun avertissement pour la déréférencement de la variable current. L’analyse statique détermine qu’il current n’est jamais déréférencé lorsqu’il est peut-être null. La variable current est vérifiée par rapport null à avant current.Parent d’être accédée et avant de passer current à l’action ProcessNode . Les exemples précédents montrent comment le compilateur détermine l’état null pour les variables locales lorsqu’elles sont initialisées, affectées ou comparées à null.

L’analyse de l’état null ne trace pas dans les méthodes appelées. Par conséquent, les champs initialisés dans une méthode d’assistance commune appelée par les constructeurs génèrent un avertissement avec le modèle suivant :

La propriété non nullable 'name' doit contenir une valeur non null lors de la sortie du constructeur.

Vous pouvez traiter ces avertissements de l’une des deux manières suivantes : chaînage du constructeur ou attributs nullables sur la méthode d’assistance. Le code suivant montre un exemple de chaque. La Person classe utilise un constructeur commun appelé par tous les autres constructeurs. La Student classe a une méthode d’assistance annotée avec l’attribut System.Diagnostics.CodeAnalysis.MemberNotNullAttribute :


using System.Diagnostics.CodeAnalysis;

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public Person(string firstName, string lastName)
    {
        FirstName = firstName;
        LastName = lastName;
    }

    public Person() : this("John", "Doe") { }
}

public class Student : Person
{
    public string Major { get; set; }

    public Student(string firstName, string lastName, string major)
        : base(firstName, lastName)
    {
        SetMajor(major);
    }

    public Student(string firstName, string lastName) :
        base(firstName, lastName)
    {
        SetMajor();
    }

    public Student()
    {
        SetMajor();
    }

    [MemberNotNull(nameof(Major))]
    private void SetMajor(string? major = default)
    {
        Major = major ?? "Undeclared";
    }
}

Notes

Un certain nombre d’améliorations apportées à l’attribution définie et à l’analyse de l’état null ont été ajoutées dans C# 10. Lorsque vous effectuez une mise à niveau vers C# 10, vous pouvez trouver moins d’avertissements nullables qui sont des faux positifs. Vous pouvez en savoir plus sur les améliorations apportées à la spécification des fonctionnalités pour les améliorations d’affectation définitives.

L’analyse de l’état nullable et les avertissements générés par le compilateur vous aident à éviter les erreurs de programme en déréférencement null. L’article sur la résolution des avertissements nullables fournit des techniques permettant de corriger les avertissements que vous verrez probablement dans votre code.

Attributs sur les signatures d’API

L’analyse de l’état null a besoin d’indicateurs de la part des développeurs pour comprendre la sémantique des API. Certaines API fournissent des vérifications null et doivent modifier l’état null d’une variable de peut-être-null à non-null. D’autres API retournent des expressions qui ne sont pas null ou peut-être-null en fonction de l’état null des arguments d’entrée. Par exemple, considérez le code suivant qui affiche un message :

public void PrintMessage(string message)
{
    if (!string.IsNullOrWhiteSpace(message))
    {
        Console.WriteLine($"{DateTime.Now}: {message}");
    }
}

En fonction de l’inspection, tout développeur considère ce code comme sûr et ne doit pas générer d’avertissements. Le compilateur ne sait pas que IsNullOrWhiteSpace fournit une vérification null. Vous appliquez des attributs pour informer le compilateur que messagen’est pas null si et uniquement si IsNullOrWhiteSpace retourne false. Dans l’exemple précédent, la signature inclut le NotNullWhen pour indiquer l’état null de message:

public static bool IsNullOrWhiteSpace([NotNullWhen(false)] string message);

Les attributs fournissent des informations détaillées sur l’état null des arguments, des valeurs de retour et des membres de l’instance d’objet utilisée pour appeler un membre. Vous trouverez plus d’informations sur chaque attribut dans l’article de référence sur les attributs de référence nullables. Les API de runtime .NET ont toutes été annotées dans .NET 5. Vous améliorez l’analyse statique en annotant vos API pour fournir des informations sémantiques sur l’état null des arguments et des valeurs de retour.

Annotations de variables nullables

L’analyse de l’état null fournit une analyse robuste pour la plupart des variables. Le compilateur a besoin de plus d’informations de votre part pour les variables membres. Le compilateur ne peut pas faire d’hypothèses sur l’ordre d’accès aux membres publics. N’importe quel membre public est accessible dans n’importe quel ordre. L’un des constructeurs accessibles peut être utilisé pour initialiser l’objet. Si un champ membre peut être défini sur null, le compilateur doit supposer que son état null est peut-être-null au début de chaque méthode.

Vous utilisez des annotations qui peuvent déclarer si une variable est un type de référence nullable ou un type de référence non nullable. Ces annotations constituent des instructions importantes concernant l’état null pour les variables :

  • Une référence n’est pas censée être null. L’état par défaut d’une variable de référence non crédable n’est pas null. Le compilateur applique des règles qui garantissent qu’il est sûr de déréférencer ces variables sans vérifier d’abord qu’elles ne sont pas null :
    • La variable doit être initialisée avec une valeur non null.
    • La variable ne peut jamais se voir attribuer la valeur null. Le compilateur émet un avertissement lorsque le code affecte une expression peut-être null à une variable qui ne doit pas être null.
  • Une référence peut être null. L’état par défaut d’une variable de référence nullable est peut-être null. Le compilateur applique des règles pour s’assurer que vous avez correctement vérifié la recherche d’une null référence :
    • La variable ne peut être déréférencée que lorsque le compilateur peut garantir que la valeur n’est pas null.
    • Ces variables peuvent être initialisées avec la valeur null par défaut et peuvent se voir attribuer la valeur null dans un autre code.
    • Le compilateur n’émet pas d’avertissements lorsque le code affecte une expression peut-être null à une variable qui peut être null.

Toute variable de référence qui n’est pas censée être null a un état null de non-null. Toute variable de référence qui peut être null initialement a l’état nullde peut-être-null.

Un type de référence nullable est inidqué avec la même syntaxe que celle des types valeur nullable : un ? est ajouté au type de la variable. Par exemple, la déclaration de variable suivante représente une variable de chaîne nullable, name :

string? name;

Toute variable pour laquelle le ? n’est pas ajouté au nom de type est un type de référence non nullable. Cela inclut toutes les variables de type de référence dans le code existant lorsque vous avez activé cette fonctionnalité. Toutefois, toutes les variables locales implicitement typées (déclarées à l’aide de var) sont des types de référence nullables. Comme l’ont montré les sections précédentes, l’analyse statique détermine l’état null des variables locales pour déterminer si elles sont peut-être null.

Parfois, vous devez remplacer un avertissement quand vous savez qu’une variable n’est pas null, mais que le compilateur détermine que son état null est peut-être-null. Vous utilisez l’opérateur !null-forgiving suivant un nom de variable pour forcer l’état null à ne pas être null. Par exemple, si vous savez que la name variable n’est pas null , mais que le compilateur émet un avertissement, vous pouvez écrire le code suivant pour remplacer l’analyse du compilateur :

name!.Length;

Les types de référence nullables et les types de valeurs nullables fournissent un concept sémantique similaire : une variable peut représenter une valeur ou un objet, ou cette variable peut être null. Toutefois, les types de référence nullables et les types de valeurs nullables sont implémentés différemment : les types de valeurs nullables sont implémentés à l’aide System.Nullable<T>de , et les types de référence nullables sont implémentés par les attributs lus par le compilateur. Par exemple, string? et string sont tous deux représentés par le même type : System.String. Toutefois, int? et int sont représentés par System.Nullable<System.Int32> et System.Int32, respectivement.

Les types de référence nullables sont une fonctionnalité de temps de compilation. Cela signifie qu’il est possible pour les appelants d’ignorer les avertissements, et d’utiliser null intentionnellement comme argument une méthode qui attend une référence non nullable. Les auteurs de bibliothèque doivent inclure des vérifications d’exécution par rapport aux valeurs d’argument null. ArgumentNullException.ThrowIfNull Est l’option préférée pour la vérification d’un paramètre par rapport à null au moment de l’exécution.

Important

L’activation des annotations nullables peut changer la façon dont Entity Framework Core détermine si un membre de données est requis. Pour plus d’informations, consultez l’article Principes de base d’Entity Framework : Utilisation de types de référence nullables.

Génériques

Les génériques nécessitent des règles détaillées à gérer T? pour n’importe quel paramètre Tde type . Les règles sont nécessairement détaillées en raison de l’historique et de l’implémentation différente d’un type valeur nullable et d’un type référence nullable. Les types de valeurs nullables sont implémentés à l’aide du System.Nullable<T> struct. Les types de référence nullables sont implémentés en tant qu’annotations de type qui fournissent des règles sémantiques au compilateur.

  • Si l’argument de type pour T est un type de référence, T? référence le type de référence nullable correspondant. Par exemple, si T est un string, est T? un string?.
  • Si l’argument de type pour T est un type valeur, T? référence le même type de valeur, T. Par exemple, si T est un int, est T? également un int.
  • Si l’argument de type pour T est un type de référence nullable, T? référence ce même type de référence nullable. Par exemple, si T est un string?, T? est également un string?.
  • Si l’argument de type pour T est un type valeur nullable, T? référence ce même type de valeur nullable. Par exemple, si T est un int?, T? est également un int?.

Pour les valeurs de retour, T? équivaut à [MaybeNull]T; pour les valeurs d’argument, T? est équivalent à [AllowNull]T. Pour plus d’informations, consultez l’article Attributs pour l’analyse de l’état null dans la référence de langage.

Vous pouvez spécifier un comportement différent à l’aide de contraintes :

  • La class contrainte signifie qu’il T doit s’agir d’un type de référence non nullable (par exemple string). Le compilateur génère un avertissement si vous utilisez un type de référence nullable, par string? exemple pour T.
  • La class? contrainte signifie qu’il T doit s’agir d’un type référence, non nullable (string) ou d’un type référence nullable (par exemple string?). Lorsque le paramètre de type est un type de référence nullable, tel que string?, expression de T? références de type de référence nullable, comme string?.
  • La notnull contrainte signifie qu’il T doit s’agir d’un type de référence non nullable ou d’un type de valeur non nullable. Si vous utilisez un type de référence nullable ou un type de valeur nullable pour le paramètre type, le compilateur génère un avertissement. En outre, quand T est un type valeur, la valeur de retour est ce type de valeur, et non le type de valeur nullable correspondant.

Ces contraintes permettent de fournir plus d’informations au compilateur sur la façon dont T ils seront utilisés. Cela est utile lorsque les développeurs choisissent le type pour T, et fournit une meilleure analyse de l’état null lorsqu’une instance du type générique est utilisée.

Contextes nullables

Les nouvelles fonctionnalités qui protègent contre la levée d’un System.NullReferenceException peuvent perturber lorsqu’elles sont activées dans un codebase existant :

  • Toutes les variables de référence typées explicitement sont interprétées comme des types de référence non nullables.
  • La signification de la class contrainte dans les génériques a été modifiée pour désigner un type de référence non nullable.
  • De nouveaux avertissements sont générés en raison de ces nouvelles règles.

Vous devez explicitement choisir d’utiliser ces fonctionnalités dans vos projets existants. Cela fournit un chemin de migration et préserve la compatibilité descendante. Les contextes nullables permettent de contrôler précisément comment le compilateur interprète les variables de type référence. Le contexte d’annotation nullable détermine le comportement du compilateur. Il existe quatre valeurs pour le contexte d’annotation nullable :

  • disable : le code est sans valeur nullable.
    • Les avertissements nullables sont désactivés.
    • Toutes les variables de type de référence sont des types de référence nullables.
    • Vous ne pouvez pas déclarer une variable en tant que type de référence nullable à l’aide du ? suffixe sur le type.
    • Vous pouvez utiliser l’opérateur de pardon null, !, mais il n’a aucun effet.
  • enable : le compilateur active toutes les analyses de référence null et toutes les fonctionnalités de langage.
    • Tous les nouveaux avertissements nullables sont activés.
    • Vous pouvez utiliser le ? suffixe pour déclarer un type de référence nullable.
    • Toutes les autres variables de type référence sont des types de référence non nullables.
    • L’opérateur de pardon null supprime les avertissements pour une affectation possible à null.
  • avertissements : le compilateur effectue toutes les analyses null et émet des avertissements lorsque le code peut déréférencer null.
    • Tous les nouveaux avertissements nullables sont activés.
    • L’utilisation du ? suffixe pour déclarer un type de référence nullable génère un avertissement.
    • Toutes les variables de type référence sont autorisées à avoir la valeur Null. Toutefois, les membres ont l’état null de not-null à l’accolade d’ouverture de toutes les méthodes, sauf s’ils sont déclarés avec le ? suffixe.
    • Vous pouvez utiliser l’opérateur de pardon null, !.
  • annotations : le compilateur n’effectue pas d’analyse null et n’émet pas d’avertissements lorsque le code peut déréférencer null.
    • Tous les nouveaux avertissements nullables sont désactivés.
    • Vous pouvez utiliser le ? suffixe pour déclarer un type de référence nullable.
    • Toutes les autres variables de type référence sont des types de référence non nullables.
    • Vous pouvez utiliser l’opérateur de pardon null, !, mais il n’a aucun effet.

Le contexte d’annotation nullable et le contexte d’avertissement nullable peuvent être définis pour un projet à l’aide de l’élément<Nullable> dans votre fichier .csproj. Cet élément configure la façon dont le compilateur interprète la nullabilité des types et les avertissements émis. Le tableau suivant montre les valeurs autorisées et résume les contextes qu’elles spécifient.

Context Avertissements de déréférence Avertissements d’affectation Types référence ? Suffixe Opérateur : !
disable Désactivé Désactivé Tous sont nullables Impossible d’utiliser N’a aucun effet
enable activé activé Non nullable, sauf si déclaré avec ? Déclare le type nullable Supprime les avertissements en cas d’affectation possible null
warnings activé Non applicable Tous sont nullables, mais les membres ne sont pas considérés comme null lors de l’ouverture des méthodes Génère un avertissement Supprime les avertissements en cas d’affectation possible null
annotations Désactivé Désactivé Non nullable, sauf si déclaré avec ? Déclare le type nullable N’a aucun effet

Les variables de type de référence dans le code compilé dans un contexte désactivé sont nullables-inconscientes. Vous pouvez affecter un null littéral ou une variable peut-être null à une variable qui peut être nullable sans oublier. Toutefois, l’état par défaut d’une variable nullable-obslivious n’est pas null.

Vous pouvez choisir le paramètre le mieux adapté à votre projet :

  • Choisissez Désactiver pour les projets hérités que vous ne souhaitez pas mettre à jour en fonction de diagnostics ou de nouvelles fonctionnalités.
  • Choisissez des avertissements pour déterminer où votre code peut lever System.NullReferenceExceptiondes s. Vous pouvez résoudre ces avertissements avant de modifier le code pour activer les types de référence non nullables.
  • Choisissez des annotations pour exprimer votre intention de conception avant d’activer les avertissements.
  • Choisissez Activer pour les nouveaux projets et les projets actifs pour lesquels vous souhaitez vous protéger contre les exceptions de référence Null.

Exemple :

<Nullable>enable</Nullable>

Vous pouvez également utiliser des directives pour définir ces mêmes contextes n’importe où dans votre code source. Celles-ci sont particulièrement utiles lorsque vous migrez une base de code volumineuse.

  • #nullable enable: définit le contexte d’annotation nullable et le contexte d’avertissement nullable à activer.
  • #nullable disable: définit le contexte d’annotation nullable et le contexte d’avertissement nullable à désactiver.
  • #nullable restore: restaure le contexte d’annotation nullable et le contexte d’avertissement nullable dans les paramètres du projet.
  • #nullable disable warnings: définissez le contexte d’avertissement nullable à désactiver.
  • #nullable enable warnings: définissez le contexte d’avertissement nullable à activer.
  • #nullable restore warnings: restaure le contexte d’avertissement nullable dans les paramètres du projet.
  • #nullable disable annotations: définissez le contexte d’annotation nullable à désactiver.
  • #nullable enable annotations: définissez le contexte d’annotation nullable à activer.
  • #nullable restore annotations: restaure le contexte d’avertissement d’annotation dans les paramètres du projet.

Pour n’importe quelle ligne de code, vous pouvez définir l’une des combinaisons suivantes :

Contexte d’avertissement Contexte d’annotation Utilisation
par défaut du projet par défaut du projet Default
enable disable Corriger les avertissements d’analyse
enable par défaut du projet Corriger les avertissements d’analyse
par défaut du projet enable Ajouter des annotations de type
enable enable Code déjà migré
disable enable Annoter du code avant de corriger les avertissements
disable disable Ajout de code hérité au projet migré
par défaut du projet disable Rarement
disable par défaut du projet Rarement

Ces neuf combinaisons vous offrent un contrôle précis sur les diagnostics émis par le compilateur pour votre code. Vous pouvez activer d’autres fonctionnalités dans n’importe quel domaine que vous mettez à jour, sans voir d’avertissements supplémentaires que vous n’êtes pas encore prêt à traiter.

Important

Le contexte global nullable ne s’applique pas aux fichiers de code générés. Dans l’une ou l’autre stratégie, le contexte nullable est désactivé pour tout fichier source marqué comme généré. Cela signifie que les API dans les fichiers générés ne sont pas annotées. Il existe quatre façons de marquer un fichier comme généré :

  1. Dans .editorconfig, spécifiez generated_code = true dans une section qui s’applique à ce fichier.
  2. Placez <auto-generated> ou <auto-generated/> dans un commentaire en haut du fichier. Il peut se trouver sur n’importe quelle ligne de ce commentaire, mais le bloc de commentaires doit être le premier élément du fichier.
  3. Démarrer le nom de fichier avec TemporaryGeneratedFile_
  4. Terminez le nom de fichier par .designer.cs, .generated.cs, .g.cs ou .g.i.cs.

Les générateurs peuvent s’inscrire à l’aide de la #nullable directive de préprocesseur.

Par défaut, les contextes d’annotation et d’avertissement nullables sont désactivés. Cela signifie que votre code existant se compile sans modification et sans générer de nouveaux avertissements. À compter de .NET 6, les nouveaux projets incluent l’élément <Nullable>enable</Nullable> dans tous les modèles de projet.

Ces options fournissent deux stratégies distinctes pour mettre à jour un code base existant afin d’utiliser des types référence nullables.

Pièges connus

Les tableaux et les structs qui contiennent des types référence sont des pièges connus dans les références nullables et l’analyse statique qui détermine la sécurité null. Dans les deux cas, une référence non nullable peut être initialisée à null, sans générer d’avertissements.

Structures

Un struct qui contient des types référence non nullables autorise l’affectation default pour celui-ci sans avertissement. Prenons l’exemple suivant :

using System;

#nullable enable

public struct Student
{
    public string FirstName;
    public string? MiddleName;
    public string LastName;
}

public static class Program
{
    public static void PrintStudent(Student student)
    {
        Console.WriteLine($"First name: {student.FirstName.ToUpper()}");
        Console.WriteLine($"Middle name: {student.MiddleName?.ToUpper()}");
        Console.WriteLine($"Last name: {student.LastName.ToUpper()}");
    }

    public static void Main() => PrintStudent(default);
}

Dans l’exemple précédent, il n’y a aucun avertissement dans PrintStudent(default) alors que les types FirstName de référence non nullables et LastName sont null.

Un autre cas plus courant concerne les structs génériques. Prenons l’exemple suivant :

#nullable enable

public struct Foo<T>
{
    public T Bar { get; set; }
}

public static class Program
{
    public static void Main()
    {
        string s = default(Foo<string>).Bar;
    }
}

Dans l’exemple précédent, la propriété Bar va être null au moment de l’exécution, et elle est affectée à une chaîne non nullable sans avertissement.

Tableaux

Les tableaux sont également un piège connu dans les types référence nullable. Prenons l’exemple suivant qui ne génère aucun avertissement :

using System;

#nullable enable

public static class Program
{
    public static void Main()
    {
        string[] values = new string[10];
        string s = values[0];
        Console.WriteLine(s.ToUpper());
    }
}

Dans l’exemple précédent, la déclaration du tableau montre qu’il contient des chaînes non nullables, tandis que ses éléments sont tous initialisés à null. Ensuite, une null valeur est affectée à la variable s (le premier élément du tableau). Enfin, la variable s est déréférencée à l’origine d’une exception d’exécution.

Voir aussi