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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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.

Au final, 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" :

 
Sélectionnez
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 :

 
Sélectionnez
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 1 élément ;
  • Create(T1,T2) pour instancier la classe Tuple<T1,T2> pour y stocker 2 éléments ;
  • Create(T1,T2,T3) pour instancier la classe Tuple<T1,T2,T3> pour y stocker 3 éléments ;
  • Create(T1,T2,T3,T4) pour instancier la classe Tuple<T1,T2,T3,T4> pour y stocker 4 éléments ;
  • Create(T1,T2,T3,T4,T5) pour instancier la classe Tuple<T1,T2,T3,T4,T5> pour y stocker 5 éléments ;
  • Create(T1,T2,T3,T4,T5,T6) pour instancier la classe Tuple<T1,T2,T3,T4,T5,T6> pour y stocker 6 é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 7 é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 8 éléments.

On pourrait donc réécrire notre méthode "GetObjectsTuple" comme suit :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 3 éléments dans un Tuple<T1,T2> :

 
Sélectionnez
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 3 é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 à 8 éléments, on aura donc :

 
Sélectionnez
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 à 9 éléments, on aura :

 
Sélectionnez
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 :

 
Sélectionnez
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 à nico-pyright(c) pour ses retours d'expériences.