Quelques raccourcis :
Depuis la version 7 du langage, C# permet de manipuler des uplets (en anglais : tuple) à travers un type nommé ValueTuple.
Contrairement à C++, où les uplets sont des types standards mais qu'une programmeuse ou un programmeur pourrait exprimer à l'aide du seul langage, les ValueTuple de C# demandent un support langagier particulier.
Supposons que nous développions un type Point3D modélisant un triplet . Supposons aussi que nous ayons un code client désireux d'afficher les valeurs de et de d'un Point3D donné :
using System;
var p = new Point3D(0,0,1);
Console.WriteLine($"x: {p.X}, y: {p.Y}");
class Point3D
{
public int X { get; } = 0;
public int Y { get; } = 0;
public int Z { get; } = 0;
public Point3D()
{
}
public Point3D(int x, int y, int z)
{
X = x;
Y = y;
Z = z;
}
// ...
}
Supposons maintenant que nous soyons intéressés au et au du Point3D, mais pas au Point3D lui-même. Avec le code existant, nous devons conserver la référence p, et nous accédons indirectement aux coordonnées souhaitées à travers cette référence (p.X, p.Y).
Nous pourrions décomposer « manuellement » p en ses parties, puis utiliser des variables temporaires par la suite, mais cela manque un peu d'élégance :
using System;
var p = new Point3D(0,0,1);
int x = p.X;
int y = p.Y;
Console.WriteLine($"x: {x}, y: {y}");
class Point3D
{
public int X { get; } = 0;
public int Y { get; } = 0;
public int Z { get; } = 0;
public Point3D()
{
}
public Point3D(int x, int y, int z)
{
X = x;
Y = y;
Z = z;
}
// ...
}
Nous pourrions aussi le décomposer manuellement avec une fonction sur mesure, ayant plusieurs extrants :
using System;
Console.WriteLine($"x: {x}, y: {y}");
Décomposer(new Point3D(0,0,1), out int x, out int y);
class Point3D
{
public int X { get; } = 0;
public int Y { get; } = 0;
public int Z { get; } = 0;
public Point3D()
{
}
public Point3D(int x, int y, int z)
{
X = x;
Y = y;
Z = z;
}
// ...
}
static void Décomposer(Point p, out int x, out int y)
{
if (p == null) throw new ArgumentNullException();
x = p.X;
y = p.Y;
}
Cette approche est toutefois un peu douloureuse, au sens où il faudrait plusieurs fonctions avec des signatures différentes si nous souhaitons des variables différentes. Par exemple, si nous voulons à la fois le , le et le , la fonction Décomposer() ci-dessus ne fera pas le travail (elle ne tient pas compte du ).
Ce que nous souhaiterions est :
Les uplets de C#, qui sont des instances d'un type un peu magique (au sens où vous ne pourriez pas l'implémenter sans un support spécial du langage) nommé ValueTuple, visent à remplir ce mandat.
À titre de premier exemple, examinez la fonction Point3D.Déconstruire() ci-dessous :
using System;
var coords = new Point3D(0,0,1).Déconstruire();
Console.WriteLine($"x: {coords.Item1}, y: {coords.Item2}");
class Point3D
{
public int X { get; } = 0;
public int Y { get; } = 0;
public int Z { get; } = 0;
public Point3D()
{
}
public Point3D(int x, int y, int z)
{
X = x;
Y = y;
Z = z;
}
public (int, int, int) Déconstruire()
{
return (X, Y, Z);
}
// ...
}
À noter :
Notre uplet, la variable coords, n'est pas un tableau ou une collection : on ne peut pas itérer sur ses éléments (c'est peut-être la raison pour laquelle le premier élément se nomme Item1, pas Item0, mais ce n'est que spéculation de ma part).
Le nommage par défaut, un peu mièvre il faut bien l'avouer, peut être remplacé de diverses manières. L'une de ces manières est de nommer les éléments à même le type de la fonction :
using System;
var coords = new Point3D(0,0,1).Déconstruire();
Console.WriteLine($"x: {coords.x}, y: {coords.y}");
class Point3D
{
public int X { get; } = 0;
public int Y { get; } = 0;
public int Z { get; } = 0;
public Point3D()
{
}
public Point3D(int x, int y, int z)
{
X = x;
Y = y;
Z = z;
}
public (int x, int y, int z) Déconstruire()
{
return (X, Y, Z);
}
// ...
}
Dans cet exemple, nous avons remplacé le type (int,int,int) par un type nommant les éléments, soit (int x, int y,int z).
Une autre option est de nommer les éléments à même le code client, au point d'appel :
using System;
(var x, var y, var z) = new Point3D(0,0,1).Déconstruire();
Console.WriteLine($"x: {x}, y: {y}");
class Point3D
{
public int X { get; } = 0;
public int Y { get; } = 0;
public int Z { get; } = 0;
public Point3D()
{
}
public Point3D(int x, int y, int z)
{
X = x;
Y = y;
Z = z;
}
public (int, int, int) Déconstruire()
{
return (X, Y, Z);
}
// ...
}
Ici, le code client récupère les trois éléments dans des variables qu'il nomme x, y et z. Avec cette syntaxe qu'est (var x,var y,var z) = ... , chacune des trois variables peut avoir n'importe quel type. Nous aurions aussi pu être plus stricts en écrivant (int x,int y,int z) = ... par exemple.
Il est possible d'ignorer un ou plusieurs éléments en remplaçant une des variables par _. Ici, puisque nous ne nous servons pas de la composante du triplet, nous aurions pu écrire (var x,var y,_) = ... tout simplement.
Si nous ne sommes pas préoccupés par la spécificité des types des éléments pris individuellement, une autre écriture, plus courte, est possible :
using System;
var (x, y, z) = new Point3D(0,0,1).Déconstruire();
Console.WriteLine($"x: {x}, y: {y}");
class Point3D
{
public int X { get; } = 0;
public int Y { get; } = 0;
public int Z { get; } = 0;
public Point3D()
{
}
public Point3D(int x, int y, int z)
{
X = x;
Y = y;
Z = z;
}
public (int, int, int) Déconstruire()
{
return (X, Y, Z);
}
// ...
}
Plusieurs combinaisons sont possibles. Par exemple, dans ce qui suit, le type de retour indique les noms des éléments alors que le code client explicite les types attendus :
using System;
(int, int, int) coords = new Point3D(0,0,1).Déconstruire();
Console.WriteLine($"x: {coords.x}, y: {coords.y}");
class Point3D
{
public int X { get; } = 0;
public int Y { get; } = 0;
public int Z { get; } = 0;
public Point3D()
{
}
public Point3D(int x, int y, int z)
{
X = x;
Y = y;
Z = z;
}
public (int x, int y, int z) Déconstruire()
{
return (X, Y, Z);
}
// ...
}
Notez que si le type de retour donne un nom à un élément d'un uplet, cela n'empêche pas le code client d'utiliser un nom qui lui convient mieux pour le même élément :
using System;
var (a, b, _) coords = new Point3D(0,0,1).Déconstruire();
Console.WriteLine($"x: {a}, y: {b}");
class Point3D
{
public int X { get; } = 0;
public int Y { get; } = 0;
public int Z { get; } = 0;
public Point3D()
{
}
public Point3D(int x, int y, int z)
{
X = x;
Y = y;
Z = z;
}
public (int x, int y, int z) Déconstruire()
{
return (X, Y, Z);
}
// ...
}
Enfin, notez qu'il est légal de nommer certains éléments d'un uplet dans un type de retour et de ne pas en nommer d'autres; ici, le type de retour de Déconstruire() pourrait être (int,int y,int) par exemple.
Bien que ce ne soit pas terriblement utile en soi (outre dans un contexte générique), il est possible de passer un uplet en paramètre à une fonction :
using System;
var (x,y) = SommePaires((2,3), (-1,0));
Console.WriteLine($"{x},{y}");
static (int,int) SommePaires((int x, int y) p0, (int x, int y) p1) => (p0.x + p1.x, p0.y + p1.y);
Ceci permet d'utiliser un uplet de la même manière que l'on utilise la plupart des autres types.
En C#, un mécanisme de déconstruction a été intégré à même le langage, et s'applique à tout type implémentant une méthode Deconstruct() de type void et avec paramètres sortants (out). Ce mécanisme n'est pas limité aux uplets, mais la décomposition au point d'appel repose sur les uplets.
using System;
var p = new Point3D(0,0,1);
var (x, y, _) = p; // appel implicite à p.Deconstruct()
Console.WriteLine($"x: {x}, y: {y}");
class Point3D
{
public int X { get; } = 0;
public int Y { get; } = 0;
public int Z { get; } = 0;
public Point3D()
{
}
public Point3D(int x, int y, int z)
{
X = x;
Y = y;
Z = z;
}
public void Deconstruct(out int x, out int y, out int z)
{
x = X;
y = Y;
z = Z;
}
// ...
}
Concrètement, dans l'exemple ci-dessus, ceci :
var p = new Point3D(0,0,1);
var (x, y, _) = p; // appel implicite à p.Deconstruct()
Console.WriteLine($"x: {x}, y: {y}");
... est équivalent à cela :
var p = new Point3D(0,0,1);
p.Deconstruct(out var x, out var y, out var z);
Console.WriteLine($"x: {x}, y: {y}");
Un appel explicite à Deconstruct() ne permet pas d'omettre un élément, contrairement à la syntaxe implicite.
Prudence : ValueTuple n'est pas limités aux types valeurs
Mieux vaut faire preuve de prudence avec les uplets de C#. En effet, bien que le type soit nommé ValueTuple, ce type ne se limite pas à des éléments qui sont eux-mêmes des types valeurs.
Portez attention à l'exemple suivant, qui remplace les int par des instances d'une classe Entier :
using System;
var pt = new Point3D(0, 0, 1);
var (x, y, z) = pt;
Console.Write($"x: {x.Valeur}, ");
Console.WriteLine($"y: {y.Valeur}");
++x.Valeur; // modifie pt.X
Console.Write $"pt.X: {pt.X.Valeur}, ");
Console.WriteLine($"pt.Y: {pt.Y.Valeur}");
class Entier
{
public int Valeur { get; set; } = 0;
}
class Point3D
{
public Entier X { get; } = new ();
public Entier Y { get; } = new ();
public Entier Z { get; } = new ();
public Point3D()
{
}
public Point3D(int x, int y, int z)
{
X.Valeur = x;
Y.Valeur = y;
Z.Valeur = y;
}
public void Deconstruct(out Entier x, out Entier y, out Entier z)
{
x = X;
y = Y;
z = Z;
}
}
Comme le montre le code client (la fonction Main()) dans cet exemple, en retournant un uplet fait de références sur des objets modifiables, il est possible pour le code client de passer par un ValueTuple pour briser l'encapsulation de l'objet déconstruit. Agissez donc avec prudence.
Quelques liens pour enrichir le propos.