Partager via


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

    L’exemple précédent est fastidieux et n’est pas recommandé.

  • Enfin, vous pouvez déconstructer le tuple en variables déjà 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.
    }
    
  • 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 des éléments ignorés, qui sont des variables en écriture seule dont vous avez choisi d’ignorer les valeurs. Vous déclarez un discard avec un caractère souligné (« _ » dans une affectation. Vous pouvez ignorer autant de valeurs que vous le souhaitez ; un seul abandon, _, représente toutes les valeurs ignorées.

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# offre un support intégré pour la déconstruction des types tuple, record, et des types 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 renvoie void. Un paramètre out dans la signature de la méthode représente chaque valeur à décomposer. Par exemple, la méthode Deconstruct suivante d’une classe Person retourne le prénom, le milieu et le nom de la famille :

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 :

  • Prénom et nom de famille.
  • Un prénom, un deuxième prénom et un nom de famille.
  • Prénom, nom de famille, nom de ville et 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 des surcharges.

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. Une variable nommée « _ » représente un abandon. Une seule opération de déconstruction peut inclure plusieurs rejets.

L’exemple suivant déconstructe un objet Person en quatre chaînes (les prénoms et les noms de famille, la ville et l’état), mais ignore le nom de la famille 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 de déconstruction

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. Le premier retourne un ensemble de valeurs qui indiquent les caractéristiques de la propriété. La seconde indique l’accessibilité de la propriété. Les valeurs booléennes indiquent si la propriété a des accesseurs get et définis distincts ou une accessibilité différente. 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 sur un System.Collections.Generic.Dictionary<TKey,TValue>, chaque élément est un KeyValuePair<TKey, TValue> et peut être déconstructé. 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.");
}

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