C# – Uplets (ValueTuple)

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.

Mise en situation

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

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 Main()
{
   var p = new Point3D(0,0,1);
   Console.WriteLine($"x: {p.X}, y: {p.Y}");
}

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 :

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 Main()
{
   var p = new Point3D(0,0,1);
   int x = p.X;
   int y = p.Y;
   Console.WriteLine($"x: {x}, y: {y}");
}

Nous pourrions aussi le décomposer manuellement avec une fonction sur mesure, ayant plusieurs extrants :

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;
}
static void Main()
{
   Décomposer(new Point3D(0,0,1), out int x, out int y);
   Console.WriteLine($"x: {x}, y: {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.

Utiliser un ValueTuple

À titre de premier exemple, examinez la fonction Point3D.Déconstruire() ci-dessous :

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);
   }
   // ...
}
static void Main()
{
   var coords = new Point3D(0,0,1).Déconstruire();
   Console.WriteLine($"x: {coords.Item1}, y: {coords.Item2}");
}

À 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).

Nommage des éléments à même le type

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 :

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);
   }
   // ...
}
static void Main()
{
   var coords = new Point3D(0,0,1).Déconstruire();
   Console.WriteLine($"x: {coords.x}, y: {coords.y}");
}

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).

Nommage des éléments au point d'appel

Une autre option est de nommer les éléments à même le code client, au point d'appel :

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);
   }
   // ...
}
static void Main()
{
   (var x, var y, var z) = new Point3D(0,0,1).Déconstruire();
   Console.WriteLine($"x: {x}, y: {y}");
}

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 :

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);
   }
   // ...
}
static void Main()
{
   var (x, y, z) = new Point3D(0,0,1).Déconstruire();
   Console.WriteLine($"x: {x}, y: {y}");
}

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 :

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);
   }
   // ...
}
static void Main()
{
   (int, int, int) coords = new Point3D(0,0,1).Déconstruire();
   Console.WriteLine($"x: {coords.x}, y: {coords.y}");
}

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 :

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);
   }
   // ...
}
static void Main()
{
   var (a, b, _) coords = new Point3D(0,0,1).Déconstruire();
   Console.WriteLine($"x: {a}, y: {b}");
}

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.

Déconstruction à même le langage

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.

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;
   }
   // ...
}
static void Main()
{
   var p = new Point3D(0,0,1);
   var (x, y, _) = p; // appel implicite à p.Deconstruct()
   Console.WriteLine($"x: {x}, y: {y}");
}

Concrètement, dans l'exemple ci-dessus, ceci :

static void Main()
{
   var p = new Point3D(0,0,1);
   var (x, y, _) = p; // appel implicite à p.Deconstruct()
   Console.WriteLine($"x: {x}, y: {y}");
}

... est équivalent à cela :

static void Main()
{
   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 :

class Program
{
   class Entier
   {
      public int Valeur { get; set; } = 0;
   }
   class Point3D
   {
      public Entier X { get; } = new Entier();
      public Entier Y { get; } = new Entier();
      public Entier Z { get; } = new Entier();
      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;
      }
   }
   static void Main(string[] args)
   {
      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}");
   }
}

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.

Lectures complémentaires

Quelques liens pour enrichir le propos.


Valid XHTML 1.0 Transitional

CSS Valide !