Les types nullables en C# 2.0

Ténière Servan

Article présentant et expliquant les types nullables apparus avec le framework 2.0

N'hésitez pas à commenter cet article ! Commentez ♪

- Titre : Les types nullables en C# 2.0

- Auteur : Ténière Servan

- Parution : 22 août 2008

- Licence : Copyright ® 2008 Ténière Servan. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- Introduction

- I. Présentation des classes concernées

- II. Le compilateur

- III. La syntaxe et les opérateurs

- III-A. Syntaxe

- III-B. Opérateurs de comparaison

- III-C. Opérateurs arithmétiques

- III-D. Opérateurs logiques

- IV. Conclusion

- V. Ressources

Introduction

Les types nullables sont une réponse à la problématique d'une valeur nulle pour les types valeurs.

Cette problématique est habituelle notamment dans le cas d'applications utilisant les bases de données relationnelles. En effet, le null d'un champ d'un enregistrement ne trouve pas d'équivalent dans les propriétés de nos objets métiers. C# 2.0 apporte une réponse…

I - Présentation des classes concernées

Analysons de plus près le code du framework pour comprendre comment fonctionnent les types nullables. Pour cela, utilisons l'excellent outil de Lutz Roeder's : Reflector qui va nous permettre de décompiler System.dll et d'analyser la structure Nullable<T> du framework.

Les génériques ont fait leur apparition dans la version 2 du framework .Net, ils ont permis d'écrire cette structure du framework qui encapsule un paramètre générique de type T. Le type valeur T est encapsulé dans un type référence. Des opérateurs de conversion implicites ont été surchargés. Un certain nombre d'autres opérateurs sont supportés (on verra comment…).

public struct Nullable<T> where T : struct
{
private bool hasValue;
internal T value;
public Nullable( T value );
public bool HasValue { get; }
public T Value { get; }
public T GetValueOrDefault();
public T GetValueOrDefault( T defaultValue );
public override bool Equals( object other );
public override int GetHashCode();
public override string ToString();
public static implicit operator Nullable<T>( T value );
public static explicit operator T( Nullable<T> value );
}

Cette structure générique possède deux propriétés principales

public bool HasValue { get; }
public T Value { get; }

HasValue est vrai pour les instances non nulles et faux pour les instances nulles.
Value nous retourne cette valeur si HasValue est vrai, dans le cas contraire une InvalidOperationException est levée.

Imaginons cette utilisation :

Nullable<int> nullable1;
Nullable<int> nullable2 = new Nullable<int>( 3 );
nullable1 = 2;
nullable1 = null;
bool testNullable1 = nullable1.HasValue;
Nullable<int> result = nullable1 * nullable2;

Cette surcharge d'opérateur de conversion

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

permet d'utiliser cette syntaxe :

b = false;

Cette surcharge d'opérateur de conversion

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

permet d'utiliser cette syntaxe :

bool test = (bool)b;

Lors de cette affectation

b = null;

c'est l'instance du type référence Nullable<T> qui est nulle, elle ne référence aucun objet.

II - Le compilateur

Déjà plusieurs questions se posent à la vue de cette classe et de son utilisation…
- Comment peut-on appeler « nullable1.HasValue» alors que nullable1 est nul… ?
- Comment l'opération de multiplication est-elle effectuée alors que l'opérateur de multiplication n'est pas surchargé dans la classe Nullable<T>… ?

C'est le compilateur qui nous apporte ces réponses. Pour s'en persuader, il suffit de décompiler notre petit exemple et de regarder quel travail le compilateur a fait sur notre code (retraduction du code IL généré en c#), voici le résultat.

On comprend maintenant que le compilateur transforme notre affectation de null par un appel au constructeur par défaut de Nullable<T>. Ainsi l'appel « nullable1.HasValue » est possible et renvoie fasse, car HasValue est initialisé à sa valeur par défaut.

Notre opération de multiplication est également modifiée, le compilateur s'occupe de tester la non-nullité des deux opérandes, et de renvoyer le résultat de la multiplication intrinsèque si ce test est vérifié. Ainsi l'opérateur * n'a pas été surchargé, c'est le travail du compilateur qui permet de supporter cet opérateur.

III - La syntaxe et les opérateurs

III-A - Syntaxe

En C# 2.0, le langage vous permet d'utiliser cette syntaxe pour les types nullables :

int? nullable1 = 2;

On n’est évidemment pas obligé de tester si HasValue est vrai avant d'accéder à Value, on peut comparer notre entier au mot clé nul comme nous le ferions pour nos types références :

int? nullable1 = 2;
int integer2;
if (nullable1 == null)
integer2 = default( int );
else
integer2 = (int)i;

Vous remarquerez que la surcharge de l'opérateur vu dans notre premier exemple (public static explicit operator T( Nullable<T> value );) nous oblige ici à effectuer un cast explicit vers int. Ce code est l'occasion pour nous de rencontrer le nouveau mot clé default qui permet de retourner une valeur par défaut en fonction du type passé en paramètre (elle renverra null pour les types références et 0 pour les types numériques).

III-B - Opérateurs de comparaison

Mais heureusement un nouvel opérateur a été introduit en C# 2.0 (null coalescing operator) qui nous permet réécrire notre code précédent ainsi

int? nullable1 = 2;
int integer2;
integer2 = nullable1 ?? default(int);

Cet opérateur ?? nous permet donc de renvoyer « nullable.Value » s'il est non nul ou default(int) dans le cas contraire.

Analysons maintenant comment se comportent les différents opérateurs avec les types nullables. Commençons par les opérateurs de comparaisons

int? nullable1 = 3;
int? nullable2 = null;
bool b = nullable1 > nullable2;                b is false
b = nullable2 > nullable1;                     b is false
b = nullable2 >= nullable1;                    b is false

Quelquesoit l'opérateur de comparaison dans ces exemples, il renvoie false dès que l'un de ces opérandes est nul. Considérons ce code :

int? nullable1 = null;
int? nullable2 = null;
bool b = nullable2 != nullable1;               b is false
b = nullable2 == nullable1;                    b is true
b = nullable2 >= nullable1;                    b is false

Les opérateurs d'égalité et de différence renvoient un résultat cohérent. L'incohérence se situe dans le fait que l'opérateur « Supérieur ou Egal » ne renvoie pas le même résultat que l'opérateur « Egal ». Il faut pour obtenir un résultat cohérent, utiliser la méthode Statique Nullable<T>.Compare

int result = Nullable.Compare<int>( nullable1, nullable2 );

if (result < 0)
{
MessageBox.Show("N1 est nul  et N2 non Null"
+ " ou" + Environment.NewLine
+ " N1 est inférieur à N2");
}
else if (result == 0)
{
MessageBox.Show( "N1 et N2 sont Null"
+ " ou" + Environment.NewLine
+ " N1 est égal à N2" );
}
else if (result > 0)
{
MessageBox.Show( "N1 est non nul et N2 est Null"
+ " ou" + Environment.NewLine
+ " N1 est supérieur à N2" );
}

III-C - Opérateurs arithmétiques

Pour ce qui est des opérateurs arithmétiques (+,-,*,…), ils renvoient pour la plupart nul lorsque l'un des opérandes est nul.

III-D - Opérateurs logiques

Les types nullables, notamment dans son utilisation avec des booléens nous font entrer dans le monde trinaire : oui, non, ou peut-être… En effet, les opérateurs logiques & et | renvoient true ou false lorsque c'est possible.

nullable1&nullable2

null

false

true

null

null

false

null

false

false

false

false

true

null

false

true

nullable1|nullable2

null

false

true

null

null

null

true

false

null

false

true

true

true

true

true

IV - Conclusion

Vous avez pu aborder à travers cet article comment les nouvelles fonctionnalités de la plateforme 2.0 permettent de résoudre ce problème de la gestion des null dans les types valeurs, et comment certains comportements sont gérés par le compilateur. Il est important de comprendre en détail ce fonctionnement, car, comme nous avons pu le constater, les résultats obtenus ne sont pas toujours intuitifs…

V - Ressources

L'article de Patrick Smacchia :
L'outil Reflector
La classe Nullable