Comprendre la possibilité de valeur Null

Effectué

Si vous êtes un développeur .NET, vous avez probablement déjà rencontré l’exception System.NullReferenceException. Cela se produit au moment de l’exécution lorsqu’une variable null est déréférencée, c’est-à-dire quand une variable est évaluée au moment de l’exécution, mais qu’elle référence null. Cette exception est de loin l’exception la plus fréquente au sein de l’écosystème .NET. Le créateur de null, Sir Tony Hoare, fait référence à null comme « l’erreur à un milliard de dollars ».

Dans l’exemple suivant, la variable FooBar est attribuée à null et immédiatement déréférencée, ce qui présente un problème :

// Declare variable and assign it as null.
FooBar fooBar = null;

// Dereference variable by calling ToString.
// This will throw a NullReferenceException.
_ = fooBar.ToString();

// The FooBar type definition.
record FooBar(int Id, string Name);

ce problème devient bien plus difficile à repérer pour les développeurs lorsque les applications évoluent en taille et en complexité. La détection des erreurs potentielles comme celle-ci est une tâche à laisser aux outils, et le compilateur C# est là pour vous aider.

Définition de la sécurité des valeurs Null

Le terme sécurité des valeurs Null définit un ensemble de fonctionnalités spécifiques aux types Nullable qui permettent de réduire le nombre d’occurrences possibles de NullReferenceException.

Dans l’exemple FooBar précédent, vous auriez pu éviter l’exception NullReferenceException en vérifiant que la variable fooBar était null avant de la déréférencer :

// Declare variable and assign it as null.
FooBar fooBar = null;

// Check for null
if (fooBar is not null)
{
    _ = fooBar.ToString();
}

// The FooBar type definition for example.
record FooBar(int Id, string Name);

Pour faciliter l’identification de scénarios comme celui-ci, le compilateur peut déduire l’intention de votre code et appliquer le comportement souhaité. Toutefois, cela se fait uniquement lorsqu’un contexte pouvant accepter la valeur Null est activé. Avant d’aborder le contexte pouvant accepter la valeur Null, nous allons décrire les types Nullable possibles.

Types Nullable

Avant C# 2.0, seuls les types référence pouvaient accepter la valeur Null. La paire types/valeur comme int ou DateTime ne pouvaient pas être null. Initialisés sans valeur, ces types reviennent à leur valeur default. Dans le cas d’un int, il s’agit de 0. Pour un DateTime, il s’agit de DateTime.MinValue.

Les types référence instanciés sans valeurs initiales fonctionnent différemment. La valeur default pour tous les types référence est null.

Prenons l’extrait de code C# suivant :

string first;                  // first is null
string second = string.Empty   // second is not null, instead it's an empty string ""
int third;                     // third is 0 because int is a value type
DateTime date;                 // date is DateTime.MinValue

Dans l’exemple précédent :

  • first est null en raison du fait que le type référence string a été déclaré, mais qu’aucune affectation n’a été effectuée.
  • second se voit attribuer string.Empty lorsqu’une déclaration a été faite. L’objet n’a jamais eu d’affectation null.
  • third est 0 même s’il n’y a pas eu d’attribution. Il s’agit d’un struct (type valeur) qui a la valeur default de 0.
  • date n’est pas initialisé, mais sa valeur default est System.DateTime.MinValue.

À compter de la version C# 2.0, vous pouvez définir des types valeur pouvant accepter la valeur Null à l’aide de Nullable<T> (ou T? si vous souhaitez utiliser un raccourci). Cela permet aux types valeur d’accepter la valeur Null. Prenons l’extrait de code C# suivant :

int? first;            // first is implicitly null (uninitialized)
int? second = null;    // second is explicitly null
int? third = default;  // third is null as the default value for Nullable<Int32> is null
int? fourth = new();    // fourth is 0, since new calls the nullable constructor

Dans l’exemple précédent :

  • first est null, car le type valeur Nullable n’a pas été initialisé.
  • second se voit attribuer null lorsqu’une déclaration a été faite.
  • third est null, car la valeur default pour Nullable<int> est null.
  • fourth est 0, car l’expression new() appelle le constructeur Nullable<int> et int est 0 par défaut.

C# 8.0 a introduit les types référence pouvant accepter les valeurs Null, dans lesquels vous pouvez exprimer votre intention qu’un type référence soitnull ou toujours non-null. Vous vous dites peut-être ceci : « Je pensais que tous les types référence acceptaient la valeur Null ! ». Et vous avez raison. Cette fonctionnalité vous permet d’exprimer votre intention, que le compilateur tentera ensuite d’appliquer. La même syntaxe T? exprime qu’un type référence est destiné à accepter la valeur Null.

Prenons l’extrait de code C# suivant :

#nullable enable

string first = string.Empty;
string second;
string? third;

À partir de l’exemple précédent, le compilateur déduit votre intention comme suit :

  • first n’est jamais null, car il a été attribué de manière définitive.
  • second ne doit jamais être null, même si au départ il s’agissait de null. Évaluer second avant l’attribution d’une valeur entraîne l’avertissement du compilateur, car il n’a pas été initialisé.
  • third peut être null. Par exemple, il peut pointer vers un System.String, mais il peut aussi pointer vers null. L’une ou l’autre de ces variantes est acceptable. Le compilateur vous aide en vous avertissant si vous déréférencez third sans vérifier d’abord qu’il n’est pas Null.

Important

Pour pouvoir utiliser la fonctionnalité de types référence pouvant accepter la valeur Null comme indiqué ci-dessus, elle doit se trouver dans un contexte pouvant accepter la valeur Null. Cela est détaillé dans la section suivante.

Contexte pouvant accepter la valeur Null

Les contextes nullables permettent de contrôler précisément comment le compilateur interprète les variables de type référence. Il existe quatre contextes possibles pouvant accepter la valeur Null :

  • disable : le compilateur se comporte de la même façon que C# 7.3 et versions antérieures.
  • enable : le compilateur active toutes les analyses de référence Null et toutes les fonctionnalités de langage.
  • warnings : le compilateur effectue toutes les analyses de valeurs Null et émet des avertissements quand le code peut déréférencer null.
  • annotations : le compilateur n’effectue pas d’analyse des valeurs Null et n’émet pas d’avertissements quand le code peut déréférencer null, mais vous pouvez toujours annoter votre code à l’aide des types référence ? pouvant accepter la valeur Null et des opérateurs null-indulgent (!).

Ce module concerne les contextes disable ou enable qui peuvent accepter la valeur Null. Pour plus d’informations, consultez Types référence pouvant accepter la valeur Null : contextes pouvant accepter la valeur Null.

Activer des types référence pouvant accepter la valeur Null

Dans le fichier projet C# (.csproj), ajoutez un nœud enfant <Nullable> à l’élément <Project> (ou ajoutez à un <PropertyGroup> existant). Cela permet d’appliquer le contexte enable pouvant accepter la valeur Null à l’ensemble du projet.

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net6.0</TargetFramework>
        <Nullable>enable</Nullable>
    </PropertyGroup>

    <!-- Omitted for brevity -->

</Project>

Vous pouvez également étendre le contexte pouvant accepter la valeur Null à un fichier C# à l’aide d’une directive de compilateur.

#nullable enable

La directive du compilateur C# précédente fonctionne comme la configuration du projet, à la différence qu’elle s’applique au fichier dans lequel elle réside. Pour plus d’informations, consultez types référence pouvant accepter la valeur Null : contextes pouvant accepter la valeur Null (documentation)

Important

Le contexte pouvant accepter la valeur Null est activé dans le fichier .csproj par défaut dans tous les modèles de projet C# à partir de .NET 6.0 et versions ultérieures.

Lorsque le contexte pouvant accepter la valeur Null est activé, vous obtenez de nouveaux avertissements. Prenons l’exemple FooBar précédent, qui affiche deux avertissements quand il est analysé dans un contexte pouvant accepter la valeur Null :

  1. La ligne FooBar fooBar = null; contient un avertissement sur l’affectation null : Avertissement C# CS8600 : conversion d’un littéral ayant une valeur Null ou d’une potentielle valeur Null en type non-nullable.

    Capture d’écran du message Avertissement C# CS8600 : conversion d’un littéral ayant une valeur Null ou d’une potentielle valeur Null en type non-nullable.

  2. La ligne _ = fooBar.ToString(); contient également un avertissement. Cette fois-ci, le compilateur vous informe que la valeur fooBar est peut être Null : Avertissement C# CS8602 : déréférencement d’une référence éventuellement Null.

    Capture d’écran du message Avertissement C# CS8602 : déréférencement d’une référence éventuellement Null.

Important

Il n’y a pas de sécurité des valeurs Null garantie, même si vous réagissez et éliminez tous les avertissements. Dans certains scénarios limités, l’analyse du compilateur aboutira tout de même à une exception NullReferenceException de runtime.

Résumé

Dans cette leçon, vous avez appris à activer un contexte pouvant accepter la valeur Null en C# pour vous protéger de l’exception NullReferenceException. Dans la leçon suivante, vous en apprendrez davantage sur l’expression explicite de votre intention dans un contexte pouvant accepter la valeur Null.