I. Introduction▲
Avec l'arrivée du .NET Framework 4, un nouveau type nommé System.Tuple a été introduit pour permettre le stockage fixe d'objets de types différents. On peut voir la classe System.Tuple comme un tableau fixe d'objets dans lequel on va pouvoir y placer des types différents d'objets qu'on aura définis à l'avance.
II. Mise en situation▲
Prenons le cas d'une méthode qui a besoin de renvoyer plus d'un élément. En tant que développeur, vous avez souvent dû vous retrouver dans cette situation que vous avez tout aussi tôt esquivée en contournant le problème. Pour les autres qui ont persisté dans cette fois, vous avez sûrement dû écrire, par exemple, la solution suivante :
class
Program
{
static
void
Main
(
string
[]
args)
{
object
[]
myObjectsArray =
GetObjectsArray
(
);
int
intParam =
(
int
)myObjectsArray[
0
];
bool
boolParam =
(
bool
)myObjectsArray[
1
];
string
stringParam =
(
string
)myObjectsArray[
2
];
System.
Console.
WriteLine
(
"{0} - {1} - {2}"
,
intParam,
boolParam,
stringParam);
}
static
object
[]
GetObjectsArray
(
)
{
object
[]
myObjects =
new
object
[
3
];
myObjects[
0
]
=
1
;
myObjects[
1
]
=
true
;
myObjects[
2
]
=
"Bonjour"
;
return
myObjects;
}
}
Il est bien sûr possible de répondre à notre problème en utilisant en valeur de retour à notre méthode, un tableau d'objets étant donné que tout type du .NET Framework dérive de la classe System.Object. Malgré cette solution tout à fait fonctionnelle, elle en reste très peu élégante, car l'exécution pourrait se passer mal si vous ne prenez pas garde à ce que vous écrivez. Des conversions incorrectes ou des erreurs d'accès au tableau d'objets pourraient très vite se glisser entraînant des exceptions à l'exécution. Le code n'est donc pas très « type-safe », car l'impact du code de conversion de type ne peut être correctement vérifié à la compilation.
Du coup, les erreurs suivantes pourraient vite arriver :
float
floatParam =
(
float
)myObjectsArray[
1
];
// InvalidCastException
short
shortParam =
(
short
)myObjectsArray[
3
];
// IndexOutOfRangeException
Une autre solution serait de créer une structure sur mesure permettant de stocker nos données :
class
Program
{
static
void
Main
(
string
[]
args)
{
TypedObjects myTypedObjects =
GetTypedObjects
(
);
System.
Console.
WriteLine
(
"{0} - {1} - {2}"
,
myTypedObjects.
MyIntParam,
myTypedObjects.
MyBoolParam,
myTypedObjects.
MyStringParam);
}
static
TypedObjects GetTypedObjects
(
)
{
TypedObjects myObjects =
new
TypedObjects
(
);
myObjects.
MyIntParam =
1
;
myObjects.
MyBoolParam =
true
;
myObjects.
MyStringParam =
"Bonjour"
;
return
myObjects;
}
}
struct
TypedObjects
{
private
int
_MyIntParam;
private
bool
_MyBoolParam;
private
string
_MyStringParam;
public
int
MyIntParam
{
get
{
return
_MyIntParam;
}
set
{
_MyIntParam =
value
;
}
}
public
bool
MyBoolParam
{
get
{
return
_MyBoolParam;
}
set
{
_MyBoolParam =
value
;
}
}
public
string
MyStringParam
{
get
{
return
_MyStringParam;
}
set
{
_MyStringParam =
value
;
}
}
}
Concernant le problème « type-safe » de la solution précédente, on est à l'abri, mais ce genre de pratique amène très vite à un projet rempli de classes et structures trop nombreuses et utilisables uniquement dans des cas très précis de notre code. Et l'on ne parle même pas de la quantité astronomique de code qu'il faut écrire en plus.
Finalement, deux solutions proposées et aucune viable dans un vrai projet. Cependant, on aura quand même relevé des avantages : un tableau d'objets simple à déclarer et initialisé dans la première solution ainsi qu'une utilisation sécurisée de nos instances dans la seconde solution.
Cela ne vous fait penser à rien ? Les génériques bien sûr !
III. La classe System.Tuple▲
La classe System.Tuple permet de regrouper des instances de classes différentes tout en offrant une sécurité au niveau des types. Cette fonctionnalité est connue par les développeurs de langages fonctionnels comme Python.
La classe System.Tuple est statique et permet la création d'un type tuple grâce à la méthode générique « Create » :
class
Program
{
static
void
Main
(
string
[]
args)
{
System.
Tuple<
int
,
bool
,
string
>
myObjectsTuple =
GetObjectsTuple
(
);
System.
Console.
WriteLine
(
"{0} - {1} - {2}"
,
myObjectsTuple.
Item1,
myObjectsTuple.
Item2,
myObjectsTuple.
Item3);
}
static
System.
Tuple<
int
,
bool
,
string
>
GetObjectsTuple
(
)
{
return
new
System.
Tuple.
Create<
int
,
bool
,
string
>(
1
,
true
,
"Bonjour"
);
}
}
Comme vous pouvez le constater dans le code précédent, on allie bien simplicité et sécurité du code.
La classe System.Tuple offre aussi sept autres surcharges de la méthode « Create » afin de pouvoir créer un tuple contenant d'un à huit éléments :
namespace
System
{
public
static
class
Tuple
{
public
static
Tuple<
T1>
Create<
T1>
(
T1 item1);
public
static
Tuple<
T1,
T2>
Create<
T1,
T2>
(
T1 item1,
T2 item2);
public
static
Tuple<
T1,
T2,
T3>
Create<
T1,
T2,
T3>
(
T1 item1,
T2 item2,
T3 item3);
public
static
Tuple<
T1,
T2,
T3,
T4>
Create<
T1,
T2,
T3,
T4>
(
T1 item1,
T2 item2,
T3 item3,
T4 item4);
public
static
Tuple<
T1,
T2,
T3,
T4,
T5>
Create<
T1,
T2,
T3,
T4,
T5>
(
T1 item1,
T2 item2,
T3 item3,
T4 item4,
T5 item5);
public
static
Tuple<
T1,
T2,
T3,
T4,
T5,
T6>
Create<
T1,
T2,
T3,
T4,
T5,
T6>
(
T1 item1,
T2 item2,
T3 item3,
T4 item4,
T5 item5,
T6 item6);
public
static
Tuple<
T1,
T2,
T3,
T4,
T5,
T6,
T7>
Create<
T1,
T2,
T3,
T4,
T5,
T6,
T7>
(
T1 item1,
T2 item2,
T3 item3,
T4 item4,
T5 item5,
T6 item6,
T7 item7);
public
static
Tuple<
T1,
T2,
T3,
T4,
T5,
T6,
T7,
Tuple<
T8>>
Create<
T1,
T2,
T3,
T4,
T5,
T6,
T7,
T8>
(
T1 item1,
T2 item2,
T3 item3,
T4 item4,
T5 item5,
T6 item6,
T7 item7,
T8 item8);
}
}
Ces surcharges de la méthode « Create » ne sont rien d'autre que des raccourcis pour instancier le type tuple générique résultant de la méthode « Create » utilisée :
- Create(T1) pour instancier la classe Tuple<T1> pour y stocker un élément ;
- Create(T1,T2) pour instancier la classe Tuple<T1,T2> pour y stocker deux éléments ;
- Create(T1,T2,T3) pour instancier la classe Tuple<T1,T2,T3> pour y stocker trois éléments ;
- Create(T1,T2,T3,T4) pour instancier la classe Tuple<T1,T2,T3,T4> pour y stocker quatre éléments ;
- Create(T1,T2,T3,T4,T5) pour instancier la classe Tuple<T1,T2,T3,T4,T5> pour y stocker cinq éléments ;
- Create(T1,T2,T3,T4,T5,T6) pour instancier la classe Tuple<T1,T2,T3,T4,T5,T6> pour y stocker six éléments ;
- Create(T1,T2,T3,T4,T5,T6,T7) pour instancier la classe Tuple<T1,T2,T3,T4,T5,T6,T7> pour y stocker sept éléments ;
- Create(T1,T2,T3,T4,T5,T6,T7,T8) pour instancier la classe Tuple<T1,T2,T3,T4,T5,T6,T7,T8> pour y stocker huit éléments.
On pourrait donc réécrire notre méthode « GetObjectsTuple » comme suit :
static
System.
Tuple<
int
,
bool
,
string
>
GetObjectsTuple2
(
)
{
return
new
System.
Tuple<
int
,
bool
,
string
>(
1
,
true
,
"Bonjour"
);
}
Il y a aussi la possibilité de simplifier l'écriture avec l'utilisation de la méthode « Create » en laissant faire l'inférence qui permet de ne pas spécifier les paramètres génériques qui seront déduits à l'aide de paramètres passés à la méthode :
static
System.
Tuple<
int
,
bool
,
string
>
GetObjectsTuple3
(
)
{
return
System.
Tuple.
Create
(
1
,
true
,
"Bonjour"
);
}
Les différentes classes Tuple (de un à huit paramètres génériques) proposent une à huit propriétés (en fonction de la classe que vous utilisez) nommées Item1, Item2, Item3, Item4, Item5, Item6, Item7 et Item8 et vous permettent de récupérer les objets que vous avez placés dans le tuple.
Ainsi, l'exemple suivant :
System.
Tuple<
bool
,
string
>
tuple1 =
System.
Tuple.
Create
(
false
,
"Skyounet"
);
bool
boolTuple1 =
tuple1.
Item1;
// Notre premier élément qui notre booléen
string
stringTuple1 =
tuple1.
Item2;
// Notre second élément qui est notre string
instancie un tuple avec deux éléments dont l'élément booléen sera accessible via la propriété Item1 et l'élément string sera accessible via la propriété Item2. Vous remarquerez aussi que les propriétés sont typées à partir de la déclaration de notre tuple, généricité oblige.
Si l'on regarde dans le .NET Framework 2.0, certains d'entre vous ont sûrement déjà eu recours à une solution similaire en détournant l'utilisation de la structure KeyValuePair<TKey, TValue> qui, à la base, permet de définir une paire clé/valeur.
IV. Les tuples limités à huit éléments ?▲
Pas du tout, étant donné que les classes System.Tuple sont génériques. On peut très bien imaginer utiliser des paramètres génériques de type System.Tuple… Vous me suivez ?
La déclaration du tuple suivant va permettre de grouper trois éléments dans un Tuple<T1,T2> :
System.
Tuple<
bool
,
System.
Tuple<
string
,
string
>>
tuple2 =
System.
Tuple.
Create
(
true
,
System.
Tuple.
Create
(
"Jérôme"
,
"Lambert"
));
bool
boolTuple2 =
tuple2.
Item1;
// Contient vrai
string
string1Tuple2 =
tuple2.
Item2.
Item1;
// Contient 'Jérôme'
string
string2Tuple2 =
tuple2.
Item2.
Item2;
// Contient 'Lambert'
Remarquez que l'exemple précédent n'est pas très intelligent, étant donné qu'il existe la classe Tuple<T1,T2,T3> qui permet de contenir trois éléments.
Par contre, si l'on veut instancier un tuple pour 8, voire plus d'éléments, la plus logique des classes est la classe Tuple<T1,T2,T3,T4,T5,T6,T7,T8> où le paramètre générique T8 doit être plus précisément un Tuple<T8>. Pour un tuple à huit éléments, on aura donc :
var
tuple3 =
System.
Tuple.
Create
(
true
,
false
,
1
,
false
,
2
,
'A'
,
"Jérôme"
,
System.
Tuple.
Create
(
"Lambert"
));
System.
Console.
WriteLine
(
tuple3);
Ce qui donnera à l'affichage : « (True, False, 1, False, 2, A, Jérôme, (Lambert)) ».
Et pour un tuple à neuf éléments, on aura :
var
tuple4 =
System.
Tuple.
Create
(
true
,
false
,
1
,
false
,
2
,
'A'
,
1
.
1
,
System.
Tuple.
Create
(
"Jérôme"
,
"Lambert"
));
System.
Console.
WriteLine
(
tuple4);
Avec comme affichage : « (True, False, 1, False, 2, A, 1,1, (Jérôme, Lambert)) ».
Comme vous l'avez compris, vous pouvez créer des tuples sans aucune limite de taille (ni de type pour rappel). Toutefois, faites attention à ne pas abuser des tuples en y stockant trop d'éléments. On a vite fait de s'y perdre au milieu des différentes propriétés nommées Itemx.
V. Égalité et comparaison avec les tuples▲
Deux nouvelles interfaces font leur apparition afin de définir leur propre logique pour tester l'égalité entre deux tuples ou bien comparer deux tuples.
- IStructuralEquatable qui offre les méthodes Equals et GetHashCode prenant toutes les deux en paramètre un IEqualityComparer.
- IStructuralComparable qui offre la méthode CompareTo prenant en paramètre un IComparer.
Pour tester votre propre algorithme de test d'égalité ou de comparaison, il vous faudra faire un transtypage de votre classe tuple en IStructuralEquatable ou IStructuralComparable selon votre besoin :
var
tuple5 =
System.
Tuple.
Create
(
true
,
false
,
"Test"
);
var
tuple6 =
System.
Tuple.
Create
(
true
,
false
,
"Test"
);
var
tuple7 =
System.
Tuple.
Create
(
false
,
true
,
"Test"
);
var
tuple8 =
System.
Tuple.
Create
(
"Test"
,
true
,
false
);
var
tuple9 =
System.
Tuple.
Create
(
true
,
false
,
"Test"
,
1
);
((
System.
Collections.
IStructuralEquatable)tuple5).
Equals
(
tuple6,
new
MyEqualityComprar
(
));
((
System.
Collections.
IStructuralComparable)tuple6).
CompareTo
(
tuple7,
new
MyComparer
(
));
VI. Conclusion▲
Comme vous avez pu le constater tout au long de cet article, ce ou ces nouveaux types System.Tuple vont permettre de combler un manque du .NET Framework qui forçait certains à utiliser des solutions peu élégantes et non sécurisées au niveau des conversions de type lors de l'exécution.
Malgré une possibilité de regroupement sans limites d'éléments au sein d'un tuple, l'utilisation de propriétés nommées Itemx risque de ralentir fortement vos ardeurs. Pensez donc aux développeurs qui passeront derrière vous, une utilisation trop intensive d'éléments dans un tuple pourrait s'avérer incompréhensible au premier coup d'œil.
Les sources des exemples tout au long de l'article ont été écrites sous Visual Studio 2010 bêta 2 et dont la solution est disponible ici.
VII. Remerciements▲
Merci à Wachter et Jean-Michel Ormes pour la relecture de cet article et à nicopyright(c) pour ses retours d'expériences.