Tuple et ValueTuple (C# 7)

Cet article fait partie d’une série d’articles sur les apports fonctionnels de C# 7 (i.e. C# 7.0/7.1/7.2/7.3).

Les tuples sont des structures de données permettant de stocker un nombre variable d’objets de type différent. L’intêret est d’éviter à avoir à déclarer la structure explicitement. Les objets sont stockés dans les membres du tuple. Les membres contenant les objets sont .Item1, .Item2, …, .Item<N>.

Historiquement, les tuples sont de type System.Tuple (apparu avec le framework .NET 4.0). System.Tuple est un type permettant de créer des objets de type référence.

Le type et le nombre de membres contenus dans le tuple sont indiqués à l’initialisation:

Tuple<int, string, float> tuple = new Tuple<int, string, float>(5, "5", 5.0f); 

Dans cet exemple, le tuple contient 3 membres:

  • Item1 de type int initialisé à la valeur 5
  • Item2 de type string initialisé à la valeur "5"
  • Item3 de type float initialisé à la valeur 5.0f

On peut aussi instancier un tuple de type System.Tuple en utilisant la syntaxe:

Tuple<int, string, float> tuple = Tuple.Create(5, "5", 5.0f);

Avant C# 7.0, les tuples devaient être utilisés exclusivement avec des noms de membres génériques (i.e. .Item1, .Item2, …, .Item<N>) ce qui rendait le code peu clair:

Tuple<int, string, float> tuple = new Tuple<int, string, float>(5, "5", 5.0f); 
Console.WriteLine(tuple.Item1); 
Console.WriteLine(tuple.Item2); 
Console.WriteLine(tuple.Item3); 

System.ValueTuple

A partir du framework .NET 4.7 est apparu le type System.ValueTuple permettant de créer des objets équivalent à System.Value. La principale différence entre ces 2 types est:

System.ValueTuple est fonctionnellement très proche de System.Tuple. Par exemple, on peut initialiser des objets System.ValueTuple avec une syntaxe semblable en utilisant la méthode statique ValueTuple.Create():

var tuple = ValueTuple.Create(5, "5", 5.0f); 

Au niveau de la syntaxe, C# 7.0 apporte des améliorations pour faciliter l’initialisation des objets de type System.ValueTuple.

Pour utiliser le type System.ValueTuple en utilisant le framework .NET 4.6.2 ou antérieur, il faut installer le package NuGet System.ValueTuple:

install-package System.ValueTuple

Amélioration à partir de C# 7.0

C# 7.0

C# 7.0 permet de rendre la syntaxe plus compacte pour initialiser des objets de type System.ValueTuple et rends plus clair l’accès à ses membres.

Initialisation

A partir de C# 7.0, on peut initialiser les objets System.ValueTuple de cette façon:

(int, string, float) tuple = (5, "5", 5.0f); 
Console.WriteLine(tuple.Item1); 
Console.WriteLine(tuple.Item2); 
Console.WriteLine(tuple.Item3); 

Utiliser des noms de membres explicites

Pour rendre l’accès aux membres plus explicite, on peut désormais nommer les membres:

(int ValueAsInt, string ValueAsString, float ValueAsFloat) tuple = (5, "5", 5.0f); 
Console.WriteLine(tuple.ValueAsInt); 
Console.WriteLine(tuple.ValueAsString); 
Console.WriteLine(tuple.ValueAsFloat); 

Une autre syntaxe est possible à l’initialisation pour indiquer les noms de membres:

var tuple = (ValueAsInt: 5, ValueAsString: "5", ValueAsFloat: 5.0f); 
Console.WriteLine(tuple.ValueAsInt); 
Console.WriteLine(tuple.ValueAsString); 
Console.WriteLine(tuple.ValueAsFloat); 

Noms de membres déterminés par des variables existantes

C# 7.1

A partir de C# 7.1, lors de l’initialisation d’un tuple, il n’est pas obligatoire de préciser le nom et le type des éléments du tuple si on l’initialise à partir de variables déjà existantes. Le nom et le type sont déterminés à partir des variables existantes:

int valueAsInt = 5; 
string valueAsString = "5"; 
float valueAsFloat = 5.0f; 
var tuple = (valueAsInt, valueAsString, valueAsFloat); // Le nom et le type des éléments du tuple 
// sont déterminés en fonction des noms et types des variables. 

Console.WriteLine(tuple.valueAsInt); 
Console.WriteLine(tuple.valueAsString); 
Console.WriteLine(tuple.valueAsFloat); 

Utiliser .Item1, .Item2, …, .Item<N> est toujours possible

C# 7.0

Même si on utilise des noms de membres dont le nom est explicite, les anciens membres .Item1, .Item2, …, .Item<N> restent toujours utilisables:

(int ValueAsInt, string ValueAsString, float ValueAsFloat) tuple = (5, "5", 5.0f); 
Console.WriteLine(tuple.Item1); 
Console.WriteLine(tuple.Item2); 
Console.WriteLine(tuple.Item3); 
System.ValueTuple est mutable

Bien-que System.ValueTuple est une structure permettant de créer des objets de type valeur, il est mutable à l’inverse de System.Tuple qui est immutable. Il est, ainsi, possible de modifier la valeur des membres .Item1, .Item2, …, .Item<N> après instanciation d’un objet de type System.ValueTuple:

(int, string, float) valObjectTuple = (5, "5", 5.0f); 
valObjectTuple.Item1 = 7; // OK 

A l’inverse, modifier un objet System.Tuple n’est pas possible:

Tuple<int, string, float> refObjectTuple = new Tuple<int, string, float>(5, "5", 5.0f);
refObjectTuple.Item1 = 7; // ERROR 

Affectation entre System.ValueTuple

C# 7.0

On peut effectuer des affectations entre objets de type System.ValueTuple en modifiant les noms des membres:

var tuple = (ValueAsInt: 5, ValueAsString: "5", ValueAsFloat: 5.0f); 
(int NewValueAsInt, string NewValueAsString, float NewValueAsFloat) newTuple = tuple; 
Console.WriteLine(newTuple.NewValueAsInt); 
Console.WriteLine(newTuple.NewValueAsString); 
Console.WriteLine(newTuple.NewValueAsFloat); 

Cette syntaxe n’est pas possible avec des objets de type System.Tuple.

Déconstruction

C# 7.0

La déconstruction permet d’affecter les membres d’un tuple dans des variables distinctes (ces syntaxes sont possibles pour les types System.Tuple et System.ValueTuple):

var tuple = ValueTuple.Create(5, "5", 5.0f); 
(int valueAsInt, string valueAsString, float valueAsFloat) = tuple; 
Console.WriteLine(valueAsInt); 
Console.WriteLine(valueAsString); 
Console.WriteLine(valueAsFloat); 

Une autre syntaxe est équivalente en utilisant le mot clé var:

var (valueAsInt, valueAsString, valueAsFloat) = tuple; 
Console.WriteLine(valueAsInt); 
Console.WriteLine(valueAsString); 
Console.WriteLine(valueAsFloat); 

Si on utilise des variables existantes:

int valueAsInt; 
string valueAsString; 
float valueAsFloat; 
(valueAsInt, valueAsString, valueAsFloat) = tuple; 
Console.WriteLine(valueAsInt); 
Console.WriteLine(valueAsString); 
Console.WriteLine(valueAsFloat); 

Ignorer une variable inutile

Lors d’une déconstruction d’un tuple, il est possible d’ignorer une variable inutile en utilisant le caractère _ de façon à alléger la syntaxe:

var tuple = (6, "6", 6.0f); 
var (ValueAsInt, ValueAsString, ValueAsFloat) = tuple; // Déconstruction du tuple 
Console.WriteLine($"Int value is {ValueAsInt}."); // ValueAsString et ValueAsFloat sont inutiles 

On peut ignorer de déclarer ces variables lors de la déconstruction:

var (ValueAsInt, _, _) = tuple; // Déconstruction 
Console.WriteLine($"Int value is {ValueAsInt}."); 

Cette syntaxe est possible pour les types System.Tuple et System.ValueTuple.

Comparaison entre tuples

La comparaison entre tuples en utilisant les opérateurs == ou != n’est pas la même suivant si on utilise des objets de type System.Tuple ou System.ValueTuple.

Comparaison entre System.Tuple

L’utilisation des opérateurs == et =! avec des tuples de type System.Tuple respecte les mêmes règles que pour tous les objets de type référence en .NET: par défaut la comparaison s’effectue sur la référence des objets:

var refTuple1 = Tuple.Create(5, "5", 5.0f); 
var refTuple2 = Tuple.Create(5, "5", 5.0f); 
Console.WriteLine(refTuple1 == refTuple2); // False 

Pour effectuer une comparaison entre les membres des objets, il faut utiliser la surcharge Equals():

Console.WriteLine(refTuple1.Equals(refTuple2); // True 

Comparaison entre System.ValueTuple

C# 7.3

Dans le cas de System.ValueTuple (avant C# 7.3), l’utilisation des opérateurs == et =! n’est pas possible, il faut utiliser CompareTo() ou Equals():

var valTuple1 = (5, "5", 5.0f); 
var valTuple2 = (5, "5", 5.0f); 
Console.WriteLine(valTuple1.CompareTo(valTuple2)); // 0 en cas d'égalité 
Console.WriteLine(valTuple1.Equals(valTuple2)); // True 

A partir de C# 7.3, il est possible d’utiliser les opérateurs == et =! pour effectuer une comparaison des membres des tuples:

Console.WriteLine(valTuple1 == valTuple2); // True

Leave a Reply