Références nullables (C# 8.0)

Cet article fait partie d’une série d’articles sur les apports fonctionnels de C# 8.0.

Cette fonctionnalité fait partie des fonctionnalités les plus importantes de C# 8.0. Elle vise à éviter que la référence d’un objet soit nulle par inadvertance.

En C#, les objets de type référence sont manipulés en utilisant une référence permettant d’atteindre l’objet stocké dans le tas managé. Les opérations les plus courantes sur une référence sont:

  • L’initialisation d’une nouvelle référence faite par copie d’une autre référence (la référence est copiée mais pas l’objet en mémoire) ou en utilisant l’opérateur new pour instancier un nouvel objet:
    Circle circle = new Circle();
    

    La classe Circle est:

    public class Circle
    {
      public int Radius;
    
      public void UpdateRadius(int newRadius)
      {
        this.radius = newRadius;
      }
    }
    
  • Le passage d’une référence en argument d’une fonction: lors de l’appel de la fonction, une copie de la référence est effectuée si elle est passée en argument (sauf si on utilise le mot-clé ref).
  • Le déférencement: cette opération permet de déférencer une référence pour utiliser l’objet dont elle fait référence en mémoire. Le déférencement permet, par exemple, d’appeler une méthode de l’objet ou d’accéder à une propriété:
    Circle circle = new Circle(); 
    circle.UpdateRadius(3); // déférencement pour appeler une fonction
    circle.Radius = 5; // déférencement pour accéder à une propriété
    

En C#, l’initialisation, l’affectation, le passage de référence sont des opérations réalisables avec une référence nulle. Le déférencement ne l’est pas, la référence nulle ne pointant sur aucun objet en mémoire. C’est ce dernier cas qui provoque le plus d’erreurs d’inadvertance puisqu’elles provoquent des erreurs de type NullReferenceException. Tony Hoare, qui est le scientifique lauréat du prix Turing à l’origine des références nulles avait qualifié son invention d’erreur à un milliard de dollars à cause de toutes les erreurs que les références nulles ont pu provoquer.

Les références nullables est une fonctionnalité de C# 8.0 visant à empêcher l’utilisation de références nulles en générant des warnings à la compilation lorsque des références potentiellement nulles sont détectées dans le code. Une référence est potentiellement nulle si elle n’est pas initialisée avec une valeur différente de null ou si elle est affectée par la valeur null.

Transformer les warnings en erreurs de compilation

Les warnings générés par le compilateur à la suite d’utilisation de référence nulles ne sont pas bloquants. Il est possible de les rendre bloquant en transformant ces warnings en erreurs en activant l’option TreatWarningsAsErrors. Pour activer cette option:

  • Dans un projet .NET Core: il faut éditer le fichier .csproj du projet et ajouter le nœud <TreatWarningsAsErrors>:
    <Project Sdk="Microsoft.NET.Sdk">  
      <PropertyGroup>  
        <OutputType>Exe</OutputType>  
        <TargetFramework>netcoreapp3.1</TargetFramework>  
        <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
      </PropertyGroup>  
    </Project> 
    
  • Dans Visual Studio, il faut éditer les options du projet:
    Dans les propriétés du projet ⇒ Onglet “Build” ⇒ dans la partie “TreatWarningsAsErrors” ⇒ sélectionner “All”.

Cette option permettra de générer des erreurs bloquantes à la compilation au lieu des warnings.

Référence nullable vs référence non-nullable

Par défaut, le comportement du compilateur est le même que pour les versions précédents C# 8.0 c’est-à-dire:

  • Le compilateur n’affiche pas de warnings à la compilation dans le cas où une référence est assignée avec null ou si un déférencement est effectué pour une référence potentiellement nulle.
  • Les références nullables ne sont pas possibles: les objets de type valeur nullables (i.e. nullable value types) sont apparus en C# 2.0. Ils sont notés avec la syntaxe <type>? par exemple int? pour un entier nullable. Cette fonctionnalité n’était valable que pour les objets de type valeur et non les objets de type référence (puisqu’une référence peut être nulle).
C# 8.0

A partir de C# 8.0, il est possible de créer des références nullables de la même façon que les objets de type valeur avec la notation <type>?, par exemple:

Circle? Circle = null; 

Le compilateur considère ainsi 2 types de références:

  • Les références non-nullables: ce sont les références normales, elles sont appelées “non-nullables” toutefois, par défaut sans modification de la configuration, elles peuvent être assignées avec la valeur nulle.
  • Les références nullables: elles sont déclarées avec la notation <type>? et elles sont nullables comme les références non-nullables toutefois, par défaut, elles génèrent des warnings si l’option Nullable n’est pas activée:
    Warning CS8632: The annotation for nullable reference types should 
      only be used in code within a '#nullable' annotations context.
    

Ces 2 types de référence prennent leur sens si on active l’option Nullable.

Activer l’option de configuration Nullable

Cette option permet de changer le comportement du compilateur vis-à-vis des références nullables et non-nullables. Il existe plusieurs niveaux de d’activation de cette option. Pour l’activer, il faut ajouter enable, warnings ou annotations dans un nœud Nullable dans le fichier .csproj du projet:

<Project Sdk="Microsoft.NET.Sdk">  
  <PropertyGroup>  
    <OutputType>Exe</OutputType>  
    <TargetFramework>netcoreapp3.1</TargetFramework>  
    <Nullable>enable</Nullable>
  </PropertyGroup>  
</Project>  

Les différents niveaux d’activation de cette option peuvent être résumés de cette façon:

Niveau Comportement général Référence nullable Référence non-nullable
enable Les références nullables sont utilisables et
des warnings sont générés si des références sont potentiellement nulles.
  • L’utilisation de références nullables ne génère pas de warnings.
  • Le déférencement d’une référence nullable potientiellement nulle génère un warning (CS8602).
  • L’affectation de null à une référence non-nullable génère un warning (CS8600).
  • Le déférencement d’une référence non-nullable potentiellement nulle génère un warning (CS8602).
warnings Les références nullables génèrent des warnings et
des warnings sont générés si des références sont potentiellement nulles.
  • L’utilisation de références nullables génère des warnings (CS8632).
  • Le déférencement d’une référence nullable potientiellement nulle génère un warning (CS8602).
annotations Les références nullables sont utilisables et
les warnings ne sont pas générés si des références sont potentiellement nulles
Pas de warnings Pas de warnings
disable
(valeur par défaut)
Les références nullables génèrent un warning et
des warnings ne sont pas générés si des références sont potentiellement nulles.
  • L’utilisation de références nullables génère des warnings (CS8632).
  • Le déférencement d’une référence nullable potientiellement nulle ne génère pas de warning.

Contexte nullable

Le contexte nullable correspond au contexte dans lequel le code est compilé. Ce contexte permet au compilateur de savoir:

  • quelles sont les règles à appliquer pour vérifier la syntaxe,
  • si des références sont nullables et
  • si des références peuvent contenir une valeur nulle et sont déférencées.

Le contexte nullable comporte 2 contextes sous-jacents:

  • Le contexte d’annotation nullable dans lequel l’utilisation des références nullables est possible et ne provoque pas de warnings.
  • Le contexte des warnings pour les références nulles dans lequel des warnings sont générés dans le cas où:
    • Une référence non-nullable est affectée avec la valeur null.
    • Une référence non-nullable est potentiellement nulle et est déférencée.

Il existe 2 méthodes pour indiquer ces contextes:

  • Par configuration: en indiquant le paramètre Nullable au niveau du fichier .csproj. De cette façon, on indique le contexte nullable dans tout le projet.

    Si on reprend le tableau plus haut, on obtient:

    Configuration Contexte d’annotation nullable Contexte des warnings pour les références nulles Référence nullable Référence non-nullable
    enable Activé Activé OK
    Warning en cas de déférencement d’une valeur nulle
    Warnings si nulle
    warnings Désactivé Non autorisé (provoque un warning)
    annotations Activé Désactivé OK
    Pas de warnings en cas de déférencement d’une valeur nulle
    Pas de warnings si nulle
    disable
    (valeur par défaut)
    Désactivé Non autorisé (provoque un warning)
  • Par code: la configuration permet d’indiquer un paramétrage pour tout le projet, ensuite il est possible d’affiner au niveau du code pour indiquer un contexte sur une portion de code. Dans le code, on peut indiquer un contexte sur une portion avec les annotations suivantes:
    Annotation Contexte d’annotation nullable Contexte des warnings pour les références nulles
    #nullable enable Activé Activé
    #nullable disable Désactivé Désactivé
    #nullable restore Les contextes sont ceux indiqués dans la configuration du projet
    #nullable enable warnings Pas de changement Activé
    #nullable disable warnings Désactivé
    #nullable restore warnings Contexte indiqué dans la configuration du projet
    #nullable enable annotations Activé Pas de changement
    #nullable disable annotations Désactivé
    #nullable restore annotations Contexte indiqué dans la configuration du projet

Se prémunir contre les valeurs nulles

La syntaxe C# prévoit quelques opérateurs pour se prémunir contre les valeurs nulles.

Opérateur !. (null-forgiving)

C# 8.0

Cet opérateur utilisable à partir de C# 8.0, est autorisé dans un contexte d’annotation nullable (c’est-à-dire quand il est possible d’utiliser des références nullables <nom variable>?). Il vise à éviter d’avoir le warning correspondant au déférencement d’une référence nullable potentiellement nulle et quand le compilateur ne peut pas déterminer si la référence est nulle ou non. Il n’a pas d’incidence sur l’exécution du code.

Par exemple, si on exécute le code suivant avec les warnings pour les références nulles activés:

int intValue = 5; 
Circle? nullableRef = null; 
if (intValue > 4) 
  nullableRef = new Circle{ Radius = 3 }; 

Console.WriteLine(nullableRef.Radius); // Warning 

A la compilation, ce code produit le warning suivant indiquant un déférencement de la référence nullableRef qui pourrait être nulle:

warning CS8602: Deference of a possible null reference. 

Pour éviter ce warning, on peut:

  • Désactiver les warnings pour les références nulles en modifiant le code de cette façon:
    #nullable disable warnings 
    
    Console.WriteLine(nullableRef.Radius); // Pas de warning
    
    #nullable enable warnings 
    
  • Utiliser l’opérateur null-forgiving:
    Console.WriteLine(nullableRef!.Radius);
    

Comme indiqué plus haut l’opérateur null-forgiving ne protège pas d’erreurs en cas de déférencement d’une référence nullable effectivement nulle:

Circle? nullableRef = null; 
Console.WriteLine(nullableRef!.Radius); // NullReferenceException 

D’autres opérateurs permettent d’éviter des erreurs provenant de références nulles.

Autres opérateurs contre les NullReferenceException

Pour se prémunir des NullReferenceException, on peut utiliser les opérateurs (la plupart de ces opérateurs existent avant C# 8.0):

Opérateur ?. (null-conditional)

L’opérateur null-conditional ?. (à partir de C# 6) s’utilise avec la syntaxe circle?.Radius.

Le comportement est:

  • si circle contient null alors la valeur de circle?.Radius est null.
  • si circle est différent de null alors circle?.Radius correspond à la valeur de Radius.

Opérateur ?[] (null-conditional)

L’opérateur null-conditional ?[] (à partir de C# 6) s’utilise avec des objets contenant des index.

Par exemple dans le cas d’un tableau:

Circle[] circles = new Circle[] { new Circle(), null }; 
Console.WriteLine(circles?[1].Radius); 

Le comportement est:

  • si circles[1] contient null alors la valeur de circles?[1].Radius est null.
  • si circles[1] est différent de null alors circles?[1].Radius correspond à la valeur de Radius.

Opérateur ?? (null-coalescing)

L’opérateur ?? permet d’évaluer si une variable est nulle.

Cet opérateur s’utilise de cette façon:

<variable à évaluer> ?? <expression si la variable est nulle> 

Ainsi:

  • Si <variable à évaluer> contient null alors l’expression retournée est <expression si la variable est nulle>.
  • Si <variable à évaluer> est différent de null alors l’expression retournée est la valeur de <variable à évaluer>.

Par exemple:

Circle firstCircle = new Circle{ Radius = 1}; 
Circle secondCircle = null; 

var radius = (secondCircle ?? firstCircle).Radius; 
Console.WriteLine(radius); // 1 c'est-à-dire la valeur de firstCircle.Radius

secondCircle = new Circle{ Radius = 2}; 

radius = (secondCircle ?? firstCircle).Radius; 
Console.WriteLine(radius); // 2 c'est-à-dire la valeur de secondCircle.Radius

A partir de C# 8.0, pour utiliser l’opérateur ?? la variable à évaluer ne peut pas être un objet de type valeur non nullable.

Opérateur ??=

C# 8.0

L’opérateur ??= permet d’affecter le résultat d’une expression à une variable si cette variable contient null (à partir de C# 8.0).

La syntaxe est du type <variable à évaluer> ??= <expression à affecter si variable null>.

Par exemple:

Circle firstCircle = new Circle{ Radius = 1 }; 
Circle? secondCircle = null; 

secondCircle ??= firstCircle; 
Console.WriteLine(secondCircle.Radius); // 1 

Dans ce code secondCircle ??= firstCircle est équivalent à:

if (secondCircle == null) 
  secondCircle = firstCircle;

Leave a Reply