Aller au contenu

Les utilités de la déconstruction d'objets

Introduction

La déconstruction est une fonctionnalité du langage C# qui permet d'obtenir plusieurs valeurs d'une structure de données dans une seule instruction. Elle est directement disponible avec les tuples et les records positionnels, mais peut également être proposée par nos propres classes et structures grâce aux méthodes Deconstruct.

Associée aux discards et au pattern matching, la déconstruction permet d'écrire un code concis et expressif.

Déconstruire un tuple

Les tuples constituent probablement l'utilisation la plus courante de la déconstruction en C#. Considérons une méthode retournant plusieurs informations concernant un utilisateur :

public static (string FirstName, string LastName, int Age) GetUser()
{
    return ("James", "Ravaille", 47);
}

Obtenir les données

Nous pouvons récupérer le tuple dans un objet et utiliser les données au travers de ses propriétés :

var user = GetUser();

Console.WriteLine(user.FirstName);
Console.WriteLine(user.LastName);
Console.WriteLine(user.Age);

Mais il est également possible de déconstruire directement le tuple en utilisant l'inférence de type (var) :

var (firstName, lastName, age) = GetUser();

Console.WriteLine(firstName);
Console.WriteLine(lastName);
Console.WriteLine(age);

Les trois valeurs contenues dans le tuple sont alors respectivement affectées aux variables firstName, lastName et age. La déconstruction permet ainsi de récupérer plusieurs valeurs dans des variables différentes avec une seule instruction.

A noter que l'utilisation de l'inférence de type (var) n'est pas obligatoire. Les types des différentes variables peuvent être précisés de manière explicite :

(string firstName, string lastName, int age) = GetUser();

Aussi, la déconstruction ne nécessite pas obligatoirement de déclarer de nouvelles variables. Elle peut être réalisée dans des variables existantes :

string firstName;
string lastName;
int age;

(firstName, lastName, age) = GetUser();

Ignorer certaines données

Il n'est pas toujours nécessaire de récupérer toutes les données d'un objet déconstruit. Le langage C# permet d'utiliser le caractère _, appelé discard, afin d'ignorer une donnée. Par exemple, si nous ne souhaitons pas obtenir l'âge :

var (firstName, lastName, _) = GetUser();

Cette syntaxe permet de rendre explicite le fait qu'une valeur existe, mais qu'elle n'est pas utile dans le traitement courant.

Déconstruire ses propres objets

La déconstruction est très souvent associée aux tuples, mais elle ne leur est pas réservée. Il est possible de rendre une classe déconstructible en y implémentant une méthode nommée Deconstruct :

public class Customer
{
    public string FirstName { get; }
    public string LastName { get; }
    public int Age { get; }

    public Customer(string firstName, string lastName, int age)
    {
        FirstName = firstName;
        LastName = lastName;
        Age = age;
    }
}

Nous pouvons accéder au contenu des propriétés pour les stocker dans des variables locales en plusieurs instructions :

var customer = new Customer("James", "Ravaille", 47);

var firstName = customer.FirstName;
var lastName = customer.LastName;
var age = customer.Age;

En implémentant une méthode nommée Deconstruct, nous pourrons faire la même chose en une seule instruction :

public class Customer
{
    // Members

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

L'objet peut désormais être déconstruit comme précédemment :

var customer = new Customer("James", "Ravaille", 47);

var (firstName, lastName, age) = customer;

Le compilateur recherche dans la classe Customer une méthode nommée Deconstruct dont les paramètres sont déclarés avec le mot-clé out. Les valeurs retournées par ces paramètres sont affectées, dans leur ordre de déclaration, aux différentes variables de la déconstruction.

Je souhaite mettre l'accent sur un point qui me semble important : la méthode Deconstruct ne retourne pas un tuple. En effet, si nous implémentons :

public (string FirstName, string LastName, int Age) Deconstruct()
{
    return (FirstName, LastName, Age);
}

Alors la méthode compile et peut être appelée explicitement :

var values = customer.Deconstruct();

Mais elle ne correspond pas au mécanisme de déconstruction défini par le langage C#. C'est pourquoi, il n'est pas possible d'écrire :

var (firstName, lastName, age) = customer;

Une méthode Deconstruct utilisée par le mécanisme de déconstruction doit être une procédure (void) et préciser les différentes valeurs à renvoyer avec des paramètres de sortie (out).

Proposer plusieurs déconstructions

Une classe peut exposer plusieurs surcharges de la méthode Deconstruct :

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

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

Et lors de la déconstruction, nous pouvons alors choisir la déconstruction adaptée :

var (firstName, lastName) = customer;

var (firstName, lastName, age) = customer;

Le nombre de variables présentes dans la déconstruction permet au compilateur de déterminer la surcharge de la méthode Deconstruct à exécuter.

Ajouter Deconstruct avec une méthode d'extension

Il est également possible de rendre un type déconstructible sans implémenter la méthode Deconstruct dans la classe, via une méthode d'extension :

public static class CustomerExtensions
{
    public static void Deconstruct(this Customer customer,
        out string firstName,
        out string lastName,
        out int age)
    {
        firstName = customer.FirstName;
        lastName = customer.LastName;
        age = customer.Age;
    }
}

Cette approche est intéressante lorsqu'il n'est pas possible de modifier le type concerné, par exemple lorsqu'il appartient à une bibliothèque externe.

Déconstruction et records

Les records occupent une place particulière concernant la déconstruction d'objets. Implémentons le record positionnel suivant :

public record Customer(string FirstName, string LastName, int Age);

Nous pouvons directement écrire :

var customer = new Customer("James", "Ravaille", 47);

var (firstName, lastName, age) = customer;

Contrairement à la classe Customer implémentée dans la première partie de cet article, nous n'avons pas eu besoin d'implémenter la méthode Deconstruct. En effet, pour les records positionnels, le compilateur génère les propriétés correspondant aux paramètres, ainsi qu'une méthode Deconstruct.

Déconstruction et pattern matching

Le mécanisme de déconstruction peut également être exploité par le pattern matching, notamment au travers des positional patterns :

public record Customer(string FirstName, string LastName, int Age);

Nous pouvons utiliser un positional pattern :

if (customer is ("James", "RAVAILLE", _))
    Console.WriteLine("Utilisateur identifié");

L'objectif ne consiste plus à affecter les valeurs du record à des variables. La représentation positionnelle est utilisée pour vérifier qu'il correspond à certaines valeurs. Ainsi, la déconstruction et le pattern matching sont deux mécanismes complémentaires.