Lancée il y a quelques mois, la nouvelle version de C# apporte de nouvelles fonctionnalités visant principalement à simplifier le code et à améliorer les performances. Au cours de cet article, nous aborderons les tuples, les nouveautés apportées par la version 7.0 de C# ainsi qu’un petit behind the scene expliquant leur fonctionnement.

Qu’avait-on dans les précédentes versions ?

Très souvent, on a besoin de retourner plusieurs valeurs. Dans les précédentes versions de C#, plusieurs solutions de contournement existent, mais elles ont toutes leurs limites :

  • Le modificateur de paramètre Out :
public void GetCoordinates(out int x, out int y)
    {
        x = 2;
        y = 2;
    }

Le mot clef Out permet de passer des arguments par référence. Mais, son utilisation n’est pas fluide et ne peut pas être utilisé avec les méthodes asynchrones.

  • Création de nouveaux types
   public class Coordinate
    {
        public int x;
        public int y;
    }

Créer une nouvelle classe pour regrouper les résultats à retourner permet de simplifier le code. Par contre, ceci n’est pas toujours un choix judicieux à effectuer en termes de design. En effet, cela rajoute plusieurs lignes de codes alors que la finalité est de regrouper de manière temporaire quelques valeurs qui n’ont pas forcément de sens ni de valeur fonctionnelle pour l’application.

  • Types anonymes
    public object GetCoordinates()
    {
        var value = new {x = 2, y = 3};
        return value;
    }

Les types anonymes permettent d’encapsuler plusieurs propriétés dans un objet unique sans définir les types explicitement. Cependant, cette solution n’est pas propre car elle est contraire au principe du typage fort.

Les tuples, une solution existante ?

La notion de tuple n’est pas nouvelle. La classe System.Tuples a fait le bonheur des développeurs lors de son apparition en C#4. Il s’agit d’une structure de données permettant de regrouper une suite d’éléments sans avoir à créer une nouvelle classe ou struct:.

On peut, comme le montre l’exemple ci-dessous, déclarer une méthode GetEmployeeInfo qui instancie et retourne un Tuple composé d’un string et d’un int.

    public Tuple<string, int> GetEmployeeInfo(string id)
    {
        //Search by id and find the employee
        return new Tuple<String, Int32>("Mark", 38000);
    }

Il est ensuite possible d’appeler la méthode et d’accéder à son contenu comme suit :

    public void Test()
    {
        Tuple<string, int> info = GetEmployeeInfo("2300-d-f");
        Console.WriteLine($"Name : {info.Item1} , Salary {info.Item2}");
    }

System.Tuples a certes été d’une grande utilité, cependant elle présente également certains inconvénients :

  • Il est obligatoire d’utiliser le mot clef Item suivi d’un chiffre indiquant l’ordre de l’élément dans le tuple (Item1, Item2…) pour accéder aux propriétés de l’objet retourné. Ce moyen d’accès ne reflète pas la signification de l’élément et devient déroutant quand il y a plusieurs éléments dans le tuple. (Dans l’exemple précédent, il aurait été mieux de pouvoir écrire : info.name et info.salry)
  • Le nombre d’éléments à retourner est capé à 8 propriétés. Pour retourner plus, il faut que le dernier élément soit un tuple : ce qui rend la syntaxe plus difficile à comprendre.
  • Il est obligatoire de déclarer est instancier un objet tuple avant de pouvoir le retourner.
  • Les tuples sont de type référence, ce qui peut être coûteux.

Les tuples en C#7

Le principe des tuples

Pour permettre de retourner multiples valeurs, le C#7 reprend le concept de System.Tuples, en exposant des tuples valeur. Il s’agit d’un ensemble ordonné fini de valeurs typées. Plus concrètement, la syntaxe a été largement simplifiée. Pour créer un tuple, il suffit de mettre le contenu entre deux parenthèses. Ensuite, le type de retour de la méthode sera sous la forme d’une parenthèse contenant les types des éléments du tuple dans leur ordre successif :

    public (string, int) GetEmployeeInfo(string id)
    {
        return ("Mark", 38000);
    }

    public void Test()
    {
        (string, int) info = GetEmployeeInfo("2300-d-f");
        Console.WriteLine($"Name : {info.Item1} , Salary {info.Item2}");
    }

Les éléments du tuple sont accessibles grâce au mot clef Item (Item 1, Item 2). Il est également possible de les nommer pour plus de simplicité.

    public (string, int) GetEmployeeInfo(string id)
    {
        return (name:"Mark", salary: 38000);
    }

    public void Test()
    {
        (string, int) info = GetEmployeeInfo("2300-d-f");
        Console.WriteLine($"Name : {info.name} , Salary {info.salary}");
    }

A la compilation, le tuple est remplacé par ValueTuple qui diffère de System.tuples par les éléments suivants :

  • Il s’agit d’une structure (type valeur), et non d’une classe (Type référence)
  • Il est mutable. La valeur de ses éléments n’est pas figée et peut être modifiée
  • Ses membres de données sont des champs et non des propriétés.

La déconstruction définie par l’utilisateur

Nous avons vu précédemment plusieurs moyens de déconstruire un tuple mais sachez que grâce au mot clé deconstruct, il est possible de spécifier au code un convertisseur implicite d’une classe vers un tuple.

Différent des autres opérateurs de conversion (keyword implicit : https://docs.microsoft.com/fr-fr/dotnet/csharp/language-reference/keywords/implicit), le deconstruct manipule les tuples de la manière suivante :

public class Employee
{
    public string FirstName { get; }
    public string LastName { get; }
    public double Salary {get; }

    public Person(string first, string last, double salary)
    {   
        FirstName = first;
        LastName = last;
        salary = salary;
    }

    public void Deconstruct(out string firstName, out string lastName)
    {
        firstName = FirstName;
        lastName = LastName;
    }
}

On peut ainsi l’utiliser comme ceci :

        var e = new Employee("john", "Doe", 1);
        var (firstname, lastName) = e;

Microsoft nous permet même de déclarer ces méthodes de déconstruction en extension et d’inférer la bonne méthode en fonction du nombre de paramètres.

public static class ExtentionsTuple
{
    public static void Deconstruct(this employee, out string firstName, out string lastName, out double salary)
    {
        first = employee.firstName;
        lastName = employee.lastName;
        salary = employee.Salary,
    }
}
        var e = new Employee("john", "Doe", 1);
        var (firstname, lastName) = e;
        var (first, last, salary) = e;

Attention, cela va sans dire qu’il y a un gros risque d’ambigüité. On peut en effet avoir étendu plusieurs fois la class Personne avec un deconstruct qui a le même nombre de paramètres.

Comment ça fonctionne ?

Tous ceux qui ont utilisé les tuples connaissent le fonctionnement : les tuples sont des classes anonymes générées à la compilation. Cependant, historiquement, les types anonymes permettaient d’appeler des propriétés par leur nom, pas les tuples. Cela était dû au fait de la structure interne du code généré par la CLR.

Une classe anonyme crée ainsi une classe par propriété afin de pouvoir inférer sans se tromper, par exemple, le code anonyme suivant :

var employee = new { First = "John", Last = "Doe", Salary = 10};

se traduisait d’un point de vue compilateur par :

En créant une classe par propriété, les types anonymes permettent l’accès directement aux propriétés nommées avec les contraintes qu’on leur connait. On est donc curieux de savoir s’il en est de même pour les Tuples. Avec l’aide de Visual Studio ((Debug > Windows > Disassembly) on se rend compte que notre méthode GetEmployeeInfo se comporte comme ceci :

Les types Tuples avec propriétés nommées sont en réalité les mêmes que les types Tuples sans propriétés nommées. Le seul ajout est la possibilité d’utiliser des noms plus parlants que Item1, Item2 etc. lsdlocke

(string First1, string Last1) person1 = ("Jon", "Doe");
(string First2, string Last2) person2 = ("Jon", "Doe");
person1 = person2;

Conclusion

Les value tuple sont de véritables sucres syntaxiques qui apportent beaucoup de simplicité et sont faciles à utiliser. Ils peuvent parfois remplacer les types et classes anonymes. Cependant, pour plus d’efficacité, les tuples doivent être utilisés seulement si le besoin est local. Si les données à regrouper sont utilisées dans plusieurs parties du code, il est plus adéquat de les encapsuler dans une classe ou structure.