Le monde magique des types nullables de C# v2 par Patrick Smacchia (patrick@smacchia.com)

 

Pré-requis. 1

Introduction. 1

La problématique d’une valeur nulle pour les types valeurs. 1

La structure System.Nullable<T>. 1

Evolution de la syntaxe C# : Nullable<T> et le mot clé null 1

Evolution de la syntaxe C# : équivalence entre Nullable<T> et T ?. 1

Ne pas utiliser las classe Nullable<T> sur un type référence. 2

Evolution de la syntaxe C# : Traitement spécial de bool?. 2

Les structures et les énumérations nullable. 2

Conclusion. 2

 

Pré-requis

Une bonne connaissance générale de C#1.

Introduction

Dans cet article, nous traiterons d’une petite caractéristique ajoutée récemment à C#2. Nous aurons ainsi en 4 articles présenté les principales évolutions du langage C#2 que sont les génériques , les itérateurs, les méthodes anonymes et maintenant les types nullables. Il y a bien sûr aussi les types partiels qui ne posent pas vraiment de problèmes de compréhension. Il y a aussi les classes statiques, les avertissements en ligne de control les tableaux fixés et quelques évolutions au niveau des délégués que nous avons mentionnées dans l’article sur les génériques.

Assez digressé! Il est temps d’exposer la problématique qui a amené les concepteurs de C#2 à ajouter les types nullables.

La problématique d’une valeur nulle pour les types valeurs

Une référence de type un type référence est nulle lorsqu’elle ne référence aucun objet. C’est la valeur prise par défaut par toute référence. Il suffit d’analyser le code de n’importe quelle application pour s’apercevoir que les développeurs exploitent les références nulles. En général, l’utilisation d’une référence nulle permet de communiquer une information :

·         Une méthode qui doit retourner une référence vers un objet retourne une référence nulle pour signifier que l’objet demandé ne peut être fabriqué ou trouvé. Cela évite d’implémenter un code d’erreur de retour binaire.

·         Lorsque vous rencontrez une méthode qui accepte un argument de type référence qui peut être nulle, cela signifie en général que l’argument est optionnel.

·         Un champ de type référence nulle peut signifier que l’objet est en cours d’initialisation, de mise à jour ou de destruction et n’a donc pas un état valide.

·         La notion de nullité est aussi largement exploitée dans les bases de données relationnelles pour signifier qu’une valeur dans un tuple n’a pas été assignée.

·         La notion de nullité peut aussi servir pour désigner un attribut optionnel dans un élément XML, etc ...

 

Parmi les nombreuses différences entre les types valeurs et les types références, nous pouvons souligner le fait qu’une instance d’un type valeur ne peut avoir de valeur nulle. Cela pose en général de nombreux problèmes. Par exemple, comment interpréter une valeur entière nulle (non encore assignée) récupérée d’une base de donnée ? De multiples solutions existent mais aucune d’entre elle n’est pleinement satisfaisante :

·         Si tout l’intervalle de valeurs entières n’est pas utilisable, on crée une convention. Par exemple, une valeur entière nulle est représentée par un entier égal à 0 ou -1. Les nombreux désavantages de cette solution sont évidents : contrainte à maintenir partout dans le code, possibilité d’évolution de l’intervalle de valeurs pris par ce champ particulier…

·         On crée une structure wrapper contenant deux champs, un entier et un booléen qui, si positionné à false, signifie que nous avons une valeur nulle. Ici, on doit gérer une structure en plus pour chaque type valeur, et un état en plus pour chaque valeur.

·         On crée une classe wrapper contenant un champ entier. Ici, le désavantage est qu’en plus du maintient d’une nouvelle classe, on surcharge le ramasse-miettes en créant de nombreux objet sur le tas.

·         On utilise le boxing, par exemple en castant notre valeur entière en une référence de type object. En plus de ne pas être type-safe, cette solution a aussi le désavantage de surcharger le ramasse-miettes.

Pour pallier ce problème récurrent, les concepteurs de C#2 ont décidé d’ajouter au langage la notion de types nullables.

La structure System.Nullable<T>

Le framework .NET2 présente la structure générique System.Nullable<T> définie (dans la beta 1 de .NET2) comme ceci :

namespace System

{

   /// <summary>Represents a value type that can contain the values of both value types such as Int32, Char, and Boolean, and reference types such as String and Version, including the reference value, null. System.Nullable is a generic type that must be bound to a value or reference type before use.</summary>

   /// <filterpriority>1</filterpriority>

   [System.Runtime.InteropServices.ComVisibleAttribute(false)]

   [System.CLSCompliantAttribute(false)]

   public struct Nullable<T> where T : struct  {  // where T :structseulement disponiblme dans la beta 2

      public bool HasValue { get; }

      public T Value { get; }

      public static bool operator !=( Nullable<T> x, Nullable<T> y);

      public static bool operator ==( Nullable<T> x, Nullable<T> y);

      public static explicit operator T(Nullable <T> value);

      public static implicit operator Nullable<T>(T value);

      public override string ToString();

      public bool Equals(Nullable<T> other);

      public override bool Equals(object other);

      public override int GetHashCode();

      // + des méthodes obsolète en beta 1!

   }

}

Cette structure répond bien à la problématique des valeurs nulles lorsque le type paramètre T prend la forme d’un type valeur telle que int. Voici un petit exemple qui illustre l’utilisation de cette structure :

static string Fct(string s){

   if (s == null)

      return null;

   return s + s;

}

static Nullable<int> Fct(Nullable<int> ni){

   if (!ni.HasValue)

      return ni;

   return (Nullable<int>) (ni.Value + ni.Value);

}

Evolution de la syntaxe C# : Nullable<T> et le mot clé null

La syntaxe C# vous permet maintenant d’assigner et de comparer le mot clé null à une instance de System.Nullable<T> :

Nullable<int> ni = null;

System.Diagnostics.Debug.Assert(ni == null);

Ce programme est équivalent à :

Nullable<int> ni = new Nullable<int>(); // appel du  constructeur par défaut qui positionne HasValue à false

System.Diagnostics.Debug.Assert(!nullable1.HasValue);

L’utilisation de la structure System.Nullable<T> est intuitive, mais peut rapidement amener à se poser des questions. Il n’est pas évident que ce programme…

Nullable<int> ni1 = 3;

Nullable<int> ni2 = 3;

bool b = (ni1 == ni2);

System.Diagnostics.Debug.Assert(b);

Nullable<int> ni3 = ni1 + ni2;

ni1++;

..soit équivalent à celui-ci.

Nullable<int> ni1 = 3;  // appel de :  implicit operator Nullable<T>(T value); qui positionne HasValue à true

Nullable<int> ni2 = 3;  // appel de :  implicit operator Nullable<T>(T value); qui positionne HasValue à true

bool b = (ni1 == ni2);  // appel de l'opérateur : bool operator ==( Nullable<T> x, Nullable<T> y);

System.Diagnostics.Debug.Assert(b);

 

Nullable<int> ni3 = null;

if (ni1.HasValue && ni2.HasValue)

   ni3 = ni1.Value + ni2.Value;

 

if( ni1.HasValue )

   ni1 = ni1.Value + 1;

Notamment, cela peut sembler étrange que la l’instruction ni1++ appelée lorsque la variable ni est sensée être null ne provoque pas une exception qui ressemblerait à NullReferenceException.

Evolution de la syntaxe C# : équivalence entre Nullable<T> et T ?

En C#2, vous pouvez faire suivre le nom d’un type T par un point d’interrogation. Dans ce cas, le compilateur de C#2 remplacera toute expression T? par Nullable<T>. Pour simplifier, vous pouvez imaginer que ceci est un prétraitement effectué directement sur le code source, un peu comme un précompilateur. Ainsi, la ligne suivante…

int? i = null;

…est équivalente à :

Nullable<int> i = null;

Ainsi les deux méthodes suivantes sont rigoureusement équivalentes:

static Nullable<int> Fct(Nullable<int> ni){

   if (!ni.HasValue)

      return ni;

   return (Nullable<int>)(ni.Value + ni.Value);

}

static int? Fct(int? ni){

   if (ni != null)

      return ni;

   return ni + ni;

}

Vous pouvez conclure qu’en général, les instances de types nullables équivalentes se mélangent bien :

int? ni1 = null;

int? ni2 = 9;

 

int? ni3 = ni1 + ni2; // OK, ni3 vaut null

int? ni4 = ni1 + 3;   // OK, ni4 vaut null

int? ni5 = ni2 + 3;   // OK, ni5 vaut 12

ni1++;                // OK, ni1 reste à  nul

ni2++;                // OK, ni2 passe à  1

Vous pouvez aussi retenir que le compilateur vous empêche de convertir implicitement un objet d’un type nullable dans le type sous-jacent. De plus, il est dangereux de réaliser une telle conversion explicitement sans tests préalables puisque vous risquer une InvalidOperationException.

int? ni1 = null;

int? ni2 = 9;

 

int i1 = ni1;         // Erreur de compilation: Cannot implicitly convert type 'int?' to 'int.

int i2 = ni2;         // Erreur de compilation: Cannot implicitly convert type 'int?' to 'int.

int i3 = ni1 + ni2;   // Erreur de compilation: Cannot implicitly convert type 'int?' to 'int.

int i4 = ni1 + 6;     // Erreur de compilation: Cannot implicitly convert type 'int?' to 'int.

 

int i5 = (int)ni1;    // OK à la compilation mais une exception de type InvalidCastException est lancée à l’exécution, car ni1 est toujours nulle

Enfin, sachez que la syntaxe avec le ‘?’ semble provenir de cette publication de Microsoft Research, qui présente quantité d’idées intéressantes pour modifier les types avec une syntaxe ‘à la regexp’.

Ne pas utiliser las classe Nullable<T> sur un type référence

Notez que si vous utilisez la syntaxe avec le point d’interrogation sur un type référence, le compilateur C#2 de la beta 1 émet un avertissement alors que le compilateur de la beta 2 émettra une erreur :

string? s = null;     // Avertissement de compilation: 'string': Nullable should only be used with value types 

En effet, il n’y a aucune raison de paramétrer la structure Nullable<T> avec un type référence. C’est pour cela que la contrainte de type valeur a été rajoutée pour la beta 2 (source forum whidbey sur les génériques). A ce sujet, voici une citation de Anders Heljsberg tirée du blog de Krzysztof Cwalina :

      ‘When you design APIs using Nullable<T>, you have to be very sensitive to the fact that a Nullable<T> is a lot less convenient to use than a T. Nullable<T> with value types makes sense and solves a real world problem--therefore, users will be amenable to the inconveniences of using it. However, Nullable<T> with reference types makes little sense and solves no real world problem. In fact, it adds nothing but confusion because, in terms capabilities, there is no difference between a string and a Nullable<string>. With respect to having a Value Type constraint on Nullable<T>, we haven't done it because it would severely limit Nullable<T>'s use in generic scenarios. For example, imagine a Find method that returns a T or a null value when an item isn't found. In the generic world, a possible solution is to return Nullable<T>, but only if Nullable<T> works for all types.’

Evolution de la syntaxe C# : Traitement spécial de bool?

Une dernière évolution de la syntaxe C# concerne l’utilisation des booléens nullables. Les mots-clés if, while et for ainsi que l’opérateur ternaire ?: interprètent un booléen nullable avec une valeur null comme si il avait une valeur false :

if   while   for   bool : true ? false l’opérateur ternaire

Par exemple :

bool? b = null;

if (b)

   System.Console.WriteLine("b est vrai");

else

   System.Console.WriteLine("b est faux ou null");

Les structures et les énumérations nullable

La notion de type nullable peut s’utiliser aussi sur vos propres structures et énumérations. Cela peut mener à des erreurs de compilations rédhibitoires illustrées par l’exemple suivant où la structure Nullable<MyStruct> ne supporte pas les membres de MyStruct.

struct MyStruct{

   public MyStruct(int i) { m_i = i; }

   public int  m_i;

   public void Fct(){}

}

class Program{

   static void Main(string[] args){

      MyStruct? ns1 = null; // OK

      MyStruct? ns2 = new MyStruct?(3);// Erreur de compilation: Cannot implicitly convert type 'int' to 'MyStruct'

      MyStruct? ns3 = new MyStruct?(); // OK MyStruct.ctor() par défaut est appelé

      MyStruct? ns4 = new MyStruct(3); // OK

      MyStruct? ns5 = new MyStruct();  // OK MyStruct.ctor() par défaut est appelé

      ns4.m_i = 8; // Erreur de compilation: System.Nullable<MyStruct>' does not contain a definition for 'm_i'

      ns4.Fct();   // Erreur de compilation: System.Nullable<MyStruct>' does not contain a definition for 'Fct'

   }

}

En revanche, si besoin est, le compilateur saura utiliser vos redéfinitions d’opérateurs :

struct MyStruct{

   public MyStruct(int i) { m_i = i; }

   public int  m_i;

   public static MyStruct operator +(MyStruct a, MyStruct b){ return new MyStruct(a.m_i + b.m_i); }

}

 

class Program{

   static void Main(string[] args){

      MyStruct? ni1 = new MyStruct(3);

      MyStruct? ni2 = new MyStruct(2);

      MyStruct? ni3 = null;

      MyStruct? ni4 = ni1 + ni2; // OK, ni4.m_i vaut 5

      MyStruct? ni5 = ni1 + ni3; // OK, ni5 vaut null

   }

}

En ce qui concerne une instance d’une énumération nullable, ayez conscience que vous devez toujours obtenir la valeur sous-jacente pour l’utiliser. Par exemple :

class Program{

   enum MyEnum { VAL1, VAL2 }

   static void Main(string[] args){

      MyEnum? e = null;

      if( e == null )

         System.Console.WriteLine("e est null");

      else

         switch(e.Value){  // ici on est sur que e n’est pas null

            case MyEnum.VAL1: System.Console.WriteLine("e vaut VAL1"); break;

            case MyEnum.VAL2: System.Console.WriteLine("e vaut VAL2"); break;

         }

   }

}

Conclusion

Nul doutes que les types nullables résolvent élégamment la problématique qu’ils adressent. Cependant, il eu été souhaitable que MS tienne aussi compte de la problématique inverse, les types non-nullables. Certains ont imaginé que l’on pourrait préfixer le nom d’un type référence avec un point d’exclamation. Une référence d’un tel type ne pourrait alors pas prendre la valeur nulle. Cela permettrait de s’affranchir dans bien souvent de la tristement célèbre NullReferenceException.

 

Références :

Nullable Types in C# blog de Eric Gunnerson

Nullable Types in C# .NET Undocumented, blog de Wesner Moise

Who wants non-nullable types (I do, I do!)?  blog de Cyrus’ Blather

Unifying Tables, Objects and Documents de Erik Meijer and Wolfram Schulte

L'utilisation des Generics dans le Framework .NET 2  blog de Yann Faure

(Non)Nullable Types in C# 2.0 blog de Luke Hutteman

Design Guidelines: Generics blog de Krzysztof Cwalina

 

 

Auteur : Patrick Smacchia

Copyright © Novembre 2004   

 

Patrick Smacchia, email [patrick@smacchia.com]

Patrick Smacchia assure de nombreuses formations sur .NET, à la fois dans l’industrie et dans le milieu universitaire (Université de Nice). Passionné par l’architecture logicielle, il aide les entreprises à concevoir et à développer leurs applications. Ingénieur diplômé de l’ENSEEIHT, il a notamment collaboré avec Amadeus la Société Générale et avec les divisions espace et téléphonie mobile d’Alcatel. Il est aussi l’auteur de l’outil open source NDepend largement adopté par la communauté des développeurs .NET. Son site expose plus en détail ses activités. Ses compétences ont été reconnues par Microsoft France, ce qui lui a valu la distinction MVP .NET (Most Valuable Professional sur les technologies .NET).
 

 L’ouvrage Pratique de .NET et C# (O’Reilly 2003)

Après avoir construit plusieurs applications d’entreprises avec C++/win32/COM/COM+/DCOM et Java/J2EE, il s’est tout naturellement intéressé à .NET. De cette rencontre est né l’ouvrage Pratique de .NET et C# (O’Reilly 2003) qui couvre la plupart des aspects du développement sous .NET avec le langage C# (architecture .NET sous jacente ; langage C# ; bibliothèques ADO.NET, XML, WinForm, GDI+… ; architectures distribuées avec COM+, .NET Remoting et ASP.NET). Cet ouvrage contient de nombreux rappels pour le rendre accessible aux étudiants et aux débutants. Les développeurs confirmés pourront quant à eux rapidement exploiter les subtiles possibilités proposées par .NET, que sont par exemple la réflexion, la programmation orientée aspect ou le mécanisme d’attribut.