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. L’utilisation d’un élément par élément est fastidieuse, comme l’illustre l’exemple suivant. La QueryCityData méthode retourne un tuple à trois tuples, 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 champ et de propriété à partir d’un objet peut être tout aussi fastidieuse : vous devez affecter une valeur de champ ou de propriété à une variable sur une base membre par membre.

Vous pouvez récupérer plusieurs éléments à partir d’un tuple ou récupérer plusieurs champs, propriétés et valeurs calculées à partir d’un objet dans une opération de déconstruction unique. Pour déconstructer 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 à 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éconstructer le tuple à trois tuples retourné par la QueryCityData méthode.

    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 à trois tuples retourné par la QueryCityData méthode.

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

    Cela est 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.
    }
    
  • À compter 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 un type spécifique en dehors des parenthèses même si chaque champ du tuple a le même type. Cela génère l’erreur du compilateur CS8136, « La déconstruction ' var (...) ' interdit un type spécifique pour 'var'. »

Vous devez 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éconstructer un tuple d’éléments « x » en variables « y ».

Éléments tuples avec des abandons

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# pour les abandons, qui sont des variables en écriture seule dont vous avez choisi d’ignorer les valeurs. Un abandon 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 QueryCityDataForYears méthode retourne un six tuple avec le nom d’une ville, sa zone, une année, la population de la ville pour cette année, une deuxième année et la population de la ville pour cette deuxième 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 record 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 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 ensuite déconstructer une instance de la Person classe nommée p avec une affectation comme le code suivant :

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.
  • Un prénom, un milieu et un nom.
  • 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 Deconstruct méthodes ayant le même nombre de paramètres sont ambiguës. Vous devez veiller à définir Deconstruct des méthodes avec différents nombres de paramètres ou « arité ». Deconstruct les méthodes 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 abandons

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 les deux accesseur get et set ont la même accessibilité, la access variable indique l’accessibilité de la propriété dans son ensemble. 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 système

Certains types de système fournissent la Deconstruct méthode comme pratique. Par exemple, le System.Collections.Generic.KeyValuePair<TKey,TValue> type fournit cette fonctionnalité. Lorsque vous effectuez une itération sur un System.Collections.Generic.Dictionary<TKey,TValue> élément est un KeyValuePair<TKey, TValue> élément qui 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.");
}

Vous pouvez ajouter une Deconstruct méthode aux types 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 Nullable<T> types d’être déconstructés dans un tuple de (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 }

record Types

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

Voir aussi