Déconstruction de tuples et d’autres types

Un tuple offre un moyen léger de récupérer plusieurs valeurs à partir d’un appel de méthode. Cependant, une fois que vous récupérez le tuple, vous devez gérer ses éléments individuels. Travailler sur une base élément par élément est assez fastidieux, comme le montre l’exemple suivant. La méthode QueryCityData retourne un tuple de trois éléments, et chacun de ses éléments est affecté à une variable dans une opération distincte.

public class Example
{
    public static void Main()
    {
        var result = QueryCityData("New York City");

        var city = result.Item1;
        var pop = result.Item2;
        var size = result.Item3;

         // Do something with the data.
    }

    private static (string, int, double) QueryCityData(string name)
    {
        if (name == "New York City")
            return (name, 8175133, 468.48);

        return ("", 0, 0);
    }
}

La récupération de plusieurs valeurs de champs et de propriétés d’un objet peut également être assez fastidieuse : vous devez affecter une valeur de champ ou de propriété à une variable membre par membre.

Vous pouvez récupérer plusieurs éléments d’un tuple ou récupérer plusieurs valeurs de champ et de propriété, ainsi que des valeurs calculées, à partir d’un objet en une seule opération de déconstruction. Pour déconstruire un tuple, vous affectez ses éléments à des variables individuelles. Quand vous déconstruisez un objet, vous affectez des valeurs sélectionnées à des variables individuelles.

Tuples

Des fonctionnalités C# intégrées prennent en charge la déconstruction des tuples, ce qui vous permet de décomposer tous les éléments d’un tuple en une seule opération. La syntaxe générale de déconstruction d’un tuple est similaire à la syntaxe qui permet d’en définir un : vous placez les variables auxquelles chaque élément doit être affecté entre des parenthèses, dans la partie gauche d’une instruction d’affectation. Par exemple, l’instruction suivante affecte les éléments d’un tuple de quatre éléments à quatre variables distinctes :

var (name, address, city, zip) = contact.GetAddressInfo();

Il existe trois façons de déconstruire un tuple :

  • Vous pouvez déclarer explicitement le type de chaque champ entre des parenthèses. L’exemple suivant utilise cette approche pour déconstruire un tuple de trois éléments retourné par la méthode QueryCityData.

    public static void Main()
    {
        (string city, int population, double area) = QueryCityData("New York City");
    
        // Do something with the data.
    }
    
  • Vous pouvez utiliser le mot clé var pour que C# infère le type de chaque variable. Vous placez le mot clé var en dehors des parenthèses. L’exemple suivant utilise l’inférence de type lors de la déconstruction du tuple de trois éléments retourné par la méthode QueryCityData.

    public static void Main()
    {
        var (city, population, area) = QueryCityData("New York City");
    
        // Do something with the data.
    }
    

    Vous pouvez aussi utiliser le mot clé var individuellement avec tout ou partie des déclarations de variables à l’intérieur des parenthèses.

    public static void Main()
    {
        (string city, var population, var area) = QueryCityData("New York City");
    
        // Do something with the data.
    }
    

    Ceci est assez fastidieux et n’est pas recommandé.

  • Enfin, vous pouvez déconstruire le tuple dans des variables qui ont déjà été déclarées.

    public static void Main()
    {
        string city = "Raleigh";
        int population = 458880;
        double area = 144.8;
    
        (city, population, area) = QueryCityData("New York City");
    
        // Do something with the data.
    }
    
  • À partir de C# 10, vous pouvez combiner la déclaration et l’affectation de variables dans une déconstruction.

    public static void Main()
    {
        string city = "Raleigh";
        int population = 458880;
    
        (city, population, double area) = QueryCityData("New York City");
    
        // Do something with the data.
    }
    

Vous ne pouvez pas spécifier de type spécifique en dehors des parenthèses, même si tous les champs du tuple ont le même type. Cela génère l’erreur du compilateur CS8136, « La déconstruction de 'var (...)' form interdit un type spécifique pour 'var'.».

Vous devez également affecter chaque élément du tuple à une variable. Si vous omettez des éléments, le compilateur génère l’erreur CS8132, « Impossible de déconstruire un tuple de « x » éléments en « y » variables ».

Éléments tuples avec des discards

Souvent, lors de la déconstruction d’un tuple, vous êtes intéressé seulement par les valeurs de certains éléments. Vous pouvez tirer parti de la prise en charge de C# des discards, qui sont des variables en écriture seule dont vous avez choisi d’ignorer les valeurs. Un discard est choisi par un caractère de soulignement (« _ ») dans une affectation. Vous pouvez ignorer autant de valeurs que vous le souhaitez ; pour représenter toutes les valeurs, utilisez l’élément ignoré unique, _.

L’exemple suivant illustre l’utilisation de tuples avec des éléments ignorés. La méthode QueryCityDataForYears retourne un tuple de six éléments avec le nom d’une ville, sa région, une année, la population de la ville pour cette année, une seconde année et la population de la ville pour cette seconde année. L’exemple montre la différence de population entre ces deux années. Parmi les données disponibles dans le tuple, nous ne sommes pas intéressés par la région de la ville, et nous connaissons le nom de la ville et les deux dates au moment du design. Par conséquent, nous sommes intéressés seulement par les deux valeurs de la population stockées dans le tuple et nous pouvons gérer ses valeurs restantes comme éléments ignorés.

using System;

public class ExampleDiscard
{
    public static void Main()
    {
        var (_, _, _, pop1, _, pop2) = QueryCityDataForYears("New York City", 1960, 2010);

        Console.WriteLine($"Population change, 1960 to 2010: {pop2 - pop1:N0}");
    }

    private static (string, double, int, int, int, int) QueryCityDataForYears(string name, int year1, int year2)
    {
        int population1 = 0, population2 = 0;
        double area = 0;

        if (name == "New York City")
        {
            area = 468.48;
            if (year1 == 1960)
            {
                population1 = 7781984;
            }
            if (year2 == 2010)
            {
                population2 = 8175133;
            }
            return (name, area, year1, population1, year2, population2);
        }

        return ("", 0, 0, 0, 0, 0);
    }
}
// The example displays the following output:
//      Population change, 1960 to 2010: 393,149

Types définis par l'utilisateur

C# n’offre pas de prise en charge intégrée pour la déconstruction de types non tuples autres que les types record et DictionaryEntry. Cependant, en tant que créateur d’une classe, d’un struct ou d’une interface, vous pouvez permettre la déconstruction du type en implémentant une ou plusieurs méthodes Deconstruct. La méthode retourne void, et chaque valeur à déconstruire est indiquée par un paramètre out dans la signature de la méthode. Par exemple, la méthode Deconstruct suivante d’une classe Person retourne le prénom, le deuxième prénom et le nom :

public void Deconstruct(out string fname, out string mname, out string lname)

Vous pouvez alors déconstruire une instance de la Personclasse nommée p avec un code comme celui-ci :

var (fName, mName, lName) = p;

L’exemple suivant surcharge la méthode Deconstruct de façon retourner différentes combinaisons des propriétés d’un objet Person. Les différentes surcharges retournent :

  • Un prénom et un nom.
  • Prénom, deuxième prénom et nom de famille.
  • Un prénom, un nom, un nom de ville et un nom d’état.
using System;

public class Person
{
    public string FirstName { get; set; }
    public string MiddleName { get; set; }
    public string LastName { get; set; }
    public string City { get; set; }
    public string State { get; set; }

    public Person(string fname, string mname, string lname,
                  string cityName, string stateName)
    {
        FirstName = fname;
        MiddleName = mname;
        LastName = lname;
        City = cityName;
        State = stateName;
    }

    // Return the first and last name.
    public void Deconstruct(out string fname, out string lname)
    {
        fname = FirstName;
        lname = LastName;
    }

    public void Deconstruct(out string fname, out string mname, out string lname)
    {
        fname = FirstName;
        mname = MiddleName;
        lname = LastName;
    }

    public void Deconstruct(out string fname, out string lname,
                            out string city, out string state)
    {
        fname = FirstName;
        lname = LastName;
        city = City;
        state = State;
    }
}

public class ExampleClassDeconstruction
{
    public static void Main()
    {
        var p = new Person("John", "Quincy", "Adams", "Boston", "MA");

        // Deconstruct the person object.
        var (fName, lName, city, state) = p;
        Console.WriteLine($"Hello {fName} {lName} of {city}, {state}!");
    }
}
// The example displays the following output:
//    Hello John Adams of Boston, MA!

Plusieurs méthodes Deconstruct ayant le même nombre de paramètres sont ambiguës. Vous devez veiller à définir des méthodes Deconstruct avec différents nombres de paramètres, ou « arity ». Les méthodes Deconstruct avec le même nombre de paramètres ne peuvent pas être distinguées lors de la résolution de surcharge.

Type défini par l’utilisateur avec des discards

Tout comme vous le faites avec des tuples, vous pouvez utiliser des éléments ignorés pour ignorer des éléments sélectionnés retournés par une méthode Deconstruct. Chaque élément ignoré est défini par une variable nommée « _ », et une même opération de déconstruction peut inclure plusieurs éléments ignorés.

L’exemple suivant déconstruit un objet Person en quatre chaînes (le prénom et le nom, la ville et l’état), mais ignore le nom et l’état.

// Deconstruct the person object.
var (fName, _, city, _) = p;
Console.WriteLine($"Hello {fName} of {city}!");
// The example displays the following output:
//      Hello John of Boston!

Méthodes d’extension pour les types définis par l’utilisateur

Si vous n’avez pas créé une classe, un struct ou une interface, vous pouvez néanmoins déconstruire des objets de ce type en implémentant une ou plusieurs Deconstructméthodes d’extension pour retourner les valeurs qui vous intéressent.

L’exemple suivant définit deux méthodes d’extension Deconstruct pour la classe System.Reflection.PropertyInfo. La première retourne un ensemble de valeurs qui indique les caractéristiques de la propriété, notamment son type, si elle est statique ou d’instance, si elle est en lecture seule et si elle est indexée. La seconde indique l’accessibilité de la propriété. Comme l’accessibilité des accesseurs get et set peut varier, des valeurs booléennes indiquent si la propriété a des accesseurs get et set distincts et, le cas échéant, s’ils ont la même accessibilité. S’il n’existe qu’un seul accesseur, ou si les deux accesseurs get et set ont la même accessibilité, la variable access indique l’accessibilité de la propriété comme un tout. Sinon, l’accessibilité des accesseurs get et set est indiquée par les variables getAccess et setAccess.

using System;
using System.Collections.Generic;
using System.Reflection;

public static class ReflectionExtensions
{
    public static void Deconstruct(this PropertyInfo p, out bool isStatic,
                                   out bool isReadOnly, out bool isIndexed,
                                   out Type propertyType)
    {
        var getter = p.GetMethod;

        // Is the property read-only?
        isReadOnly = ! p.CanWrite;

        // Is the property instance or static?
        isStatic = getter.IsStatic;

        // Is the property indexed?
        isIndexed = p.GetIndexParameters().Length > 0;

        // Get the property type.
        propertyType = p.PropertyType;
    }

    public static void Deconstruct(this PropertyInfo p, out bool hasGetAndSet,
                                   out bool sameAccess, out string access,
                                   out string getAccess, out string setAccess)
    {
        hasGetAndSet = sameAccess = false;
        string getAccessTemp = null;
        string setAccessTemp = null;

        MethodInfo getter = null;
        if (p.CanRead)
            getter = p.GetMethod;

        MethodInfo setter = null;
        if (p.CanWrite)
            setter = p.SetMethod;

        if (setter != null && getter != null)
            hasGetAndSet = true;

        if (getter != null)
        {
            if (getter.IsPublic)
                getAccessTemp = "public";
            else if (getter.IsPrivate)
                getAccessTemp = "private";
            else if (getter.IsAssembly)
                getAccessTemp = "internal";
            else if (getter.IsFamily)
                getAccessTemp = "protected";
            else if (getter.IsFamilyOrAssembly)
                getAccessTemp = "protected internal";
        }

        if (setter != null)
        {
            if (setter.IsPublic)
                setAccessTemp = "public";
            else if (setter.IsPrivate)
                setAccessTemp = "private";
            else if (setter.IsAssembly)
                setAccessTemp = "internal";
            else if (setter.IsFamily)
                setAccessTemp = "protected";
            else if (setter.IsFamilyOrAssembly)
                setAccessTemp = "protected internal";
        }

        // Are the accessibility of the getter and setter the same?
        if (setAccessTemp == getAccessTemp)
        {
            sameAccess = true;
            access = getAccessTemp;
            getAccess = setAccess = String.Empty;
        }
        else
        {
            access = null;
            getAccess = getAccessTemp;
            setAccess = setAccessTemp;
        }
    }
}

public class ExampleExtension
{
    public static void Main()
    {
        Type dateType = typeof(DateTime);
        PropertyInfo prop = dateType.GetProperty("Now");
        var (isStatic, isRO, isIndexed, propType) = prop;
        Console.WriteLine($"\nThe {dateType.FullName}.{prop.Name} property:");
        Console.WriteLine($"   PropertyType: {propType.Name}");
        Console.WriteLine($"   Static:       {isStatic}");
        Console.WriteLine($"   Read-only:    {isRO}");
        Console.WriteLine($"   Indexed:      {isIndexed}");

        Type listType = typeof(List<>);
        prop = listType.GetProperty("Item",
                                    BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
        var (hasGetAndSet, sameAccess, accessibility, getAccessibility, setAccessibility) = prop;
        Console.Write($"\nAccessibility of the {listType.FullName}.{prop.Name} property: ");

        if (!hasGetAndSet | sameAccess)
        {
            Console.WriteLine(accessibility);
        }
        else
        {
            Console.WriteLine($"\n   The get accessor: {getAccessibility}");
            Console.WriteLine($"   The set accessor: {setAccessibility}");
        }
    }
}
// The example displays the following output:
//       The System.DateTime.Now property:
//          PropertyType: DateTime
//          Static:       True
//          Read-only:    True
//          Indexed:      False
//
//       Accessibility of the System.Collections.Generic.List`1.Item property: public

Méthode d’extension pour les types de système

Certains types de système fournissent la méthode Deconstruct par commodité. Par exemple, le type System.Collections.Generic.KeyValuePair<TKey,TValue> fournit cette fonctionnalité. Lorsque vous effectuez une itération surSystem.Collections.Generic.Dictionary<TKey,TValue>, chaque élément est un KeyValuePair<TKey, TValue> et peut être déconstruit. Prenons l’exemple suivant :

Dictionary<string, int> snapshotCommitMap = new(StringComparer.OrdinalIgnoreCase)
{
    ["https://github.com/dotnet/docs"] = 16_465,
    ["https://github.com/dotnet/runtime"] = 114_223,
    ["https://github.com/dotnet/installer"] = 22_436,
    ["https://github.com/dotnet/roslyn"] = 79_484,
    ["https://github.com/dotnet/aspnetcore"] = 48_386
};

foreach (var (repo, commitCount) in snapshotCommitMap)
{
    Console.WriteLine(
        $"The {repo} repository had {commitCount:N0} commits as of November 10th, 2021.");
}

Vous pouvez ajouter une méthode Deconstruct aux types de système qui n’en ont pas. Considérez la méthode d’extension suivante :

public static class NullableExtensions
{
    public static void Deconstruct<T>(
        this T? nullable,
        out bool hasValue,
        out T value) where T : struct
    {
        hasValue = nullable.HasValue;
        value = nullable.GetValueOrDefault();
    }
}

Cette méthode d’extension permet à tous les types Nullable<T> d’être déconstruits dans un tuple (bool hasValue, T value). L’exemple suivant montre le code qui utilise cette méthode d’extension :

DateTime? questionableDateTime = default;
var (hasValue, value) = questionableDateTime;
Console.WriteLine(
    $"{{ HasValue = {hasValue}, Value = {value} }}");

questionableDateTime = DateTime.Now;
(hasValue, value) = questionableDateTime;
Console.WriteLine(
    $"{{ HasValue = {hasValue}, Value = {value} }}");

// Example outputs:
// { HasValue = False, Value = 1/1/0001 12:00:00 AM }
// { HasValue = True, Value = 11/10/2021 6:11:45 PM }

Types record

Lorsque vous déclarez un type d’enregistrement à l’aide de deux paramètres positionnels ou plus, le compilateur crée une méthode Deconstruct avec un paramètre out pour chaque paramètre positionnel dans la déclaration record. Pour plus d’informations, consultez Syntaxe positionnelle pour la définition de propriété et Comportement de déconstruction dans les enregistrements dérivés.

Voir aussi