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…