Деконструкция кортежей и других типов

Кортеж позволяет вам легко получить несколько значений при вызове метода. Но после получения кортежа вам нужно будет обработать его отдельные элементы. Работа с элементом по элементам является громоздкой, как показано в следующем примере. Метод QueryCityData возвращает три кортежа, и каждый из его элементов назначается переменной в отдельной операции.

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);
    }
}

Получение нескольких значений полей и свойств из объекта может быть столь же трудоемким: необходимо назначить переменной значение поля или свойства на основе члена.

Можно извлечь несколько элементов из кортежа или получить несколько полей, свойств и вычисляемых значений из объекта в рамках одной операции деконструкции . Чтобы деконструировать кортеж, необходимо назначить его элементы отдельным переменным. При деконструкции объекта вы присваиваете отдельным переменным выбранные значения.

Кортежи

Язык C# имеет встроенную поддержку деконструкции кортежей, которая позволяет извлекать из кортежа все элементы за одну операцию. Общий синтаксис деконструкции кортежа напоминает синтаксис его определения: переменные, которым будут присвоены элементы кортежа, указываются в круглых скобках в левой части оператора присваивания. Например, следующая инструкция назначает элементы четырех кортежей четырем отдельным переменным:

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

Существует три способа деконструкции кортежа:

  • Вы можете явно объявить тип каждого поля в скобках. В следующем примере этот подход используется для деконструкции трех кортежей, возвращаемых методом QueryCityData .

    public static void Main()
    {
        (string city, int population, double area) = QueryCityData("New York City");
    
        // Do something with the data.
    }
    
  • Вы можете использовать ключевое слово var, чтобы C# определил тип каждой переменной. Ключевое слово var помещается за пределами скобок. В следующем примере при деконструкции трех кортежей, возвращаемых методом QueryCityData , используется определение типа.

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

    Кроме того, вы можете использовать ключевое слово var при объявлении отдельных или всех переменных внутри скобок.

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

    Это громоздкий и не рекомендуется.

  • Наконец, можно выполнить деконструкцию кортежа в переменные, которые уже были объявлены.

    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.
    }
    
  • Начиная с C# 10, можно смешивать объявление переменных и назначение в деконструкции.

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

Нельзя указать определенный тип вне круглых скобок, даже если каждое поле кортежа имеет один и тот же тип. При этом возникает ошибка компилятора CS8136, форма deconstruction "var (...)" запрещает определенный тип для "var".

Необходимо назначить каждому элементу кортежа переменной. Если опустить какие-либо элементы, компилятор создает ошибку CS8132: "Не удается деконструировать кортеж элементов x в переменные y".

Элементы кортежа с отменами

При деконструкции кортежа нас часто интересуют значения только некоторых элементов. Вы можете воспользоваться преимуществами поддержки C# для отклонений, которые являются переменными только для записи, значения которых вы решили игнорировать. Отмена выбирается символом подчеркивания ("_") в назначении. Вы можете сделать пустыми сколько угодно значений. Все они будут считаться одной переменной, _.

В следующем примере показано использование кортежей с пустыми переменными. Метод QueryCityDataForYears возвращает шесть кортежей с именем города, его области, годом, населением города за этот год, вторым годом и населением города на этот второй год. В примере показано изменение численности населения за эти два года. Из доступных в кортеже данных нас не интересует площадь города, а название города и две даты известны нам уже на этапе разработки. Следовательно, нас интересуют только два значения численности населения, которые хранятся в кортеже. Остальные значения можно обработать как пустые переменные.

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

Определяемые пользователем типы

C# не поддерживает встроенную поддержку деконструкции типов не кортежей, отличных от record типов DictionaryEntry . Тем не менее, если вы являетесь создателем класса, структуры или интерфейса, вы можете разрешить деконструкцию экземпляров определенного типа, реализовав один или несколько методов Deconstruct. Метод возвращает "void", и каждое деконструируемое значение обозначается параметром out в сигнатуре метода. Например, следующий метод Deconstruct класса Person возвращает имя, отчество и фамилию:

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

Затем можно деконструировать экземпляр Person класса с именем p назначения, как показано в следующем коде:

var (fName, mName, lName) = p;

В следующем примере показана перегрузка метода Deconstruct для возвращения различных сочетаний свойств объекта Person. Отдельные перегрузки возвращают следующие значения:

  • Имя и фамилия.
  • Имя, отчество, фамилия.
  • Имя, фамилия, название города и название штата.
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!

Несколько методов Deconstruct с одинаковым числом параметров вносят путаницу. Старайтесь определять методы Deconstruct с разным числом параметров или аргументов. Методы Deconstruct с одинаковым количеством параметров невозможно различить при разрешении перегрузки.

Определяемый пользователем тип с отменами

Как и с кортежами, пустые переменные можно применять с пользовательскими типами, чтобы игнорировать определенные элементы, возвращаемые методом Deconstruct. Каждая пустая переменная определяется переменной с именем "_", и одна операция деконструкции может включать несколько пустых переменных.

В следующем примере показана деконструкция объекта Person на четыре строки (имя, фамилия, город и область), но для фамилии и области используются пустые переменные.

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

Методы расширения для определяемых пользователем типов

Если вы не создали класс, структуру или интерфейс, можно по-прежнему деконструировать объекты этого типа, реализовав один или несколько Deconstructметодов расширения , чтобы вернуть интересующие вас значения.

В приведенном ниже примере определены два метода расширения Deconstruct для класса System.Reflection.PropertyInfo. Первый метод возвращает набор значений, которые указывают характеристики свойства, в том числе его тип, является ли оно статическим свойством или экземпляром, доступно ли оно только для чтения и является ли оно индексируемым. Второй метод показывает уровень доступа свойства. Так как методы доступа для чтения и записи у свойства могут иметь разный уровень доступа, мы используем логические значения, которые показывают, имеет ли свойство разные методы для чтения и записи и, если это так, имеют ли эти методы один уровень доступа. Если имеется только один метод доступа или как метод получения, так и метод доступа set имеют одинаковые специальные возможности, access переменная указывает на доступность свойства в целом. В противном случае доступность методов чтения и записи указывается переменными getAccess и 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

Метод расширения для системных типов

Некоторые системные типы предоставляют Deconstruct метод в качестве удобства. Например, System.Collections.Generic.KeyValuePair<TKey,TValue> тип предоставляет эту функцию. При итерации по каждому System.Collections.Generic.Dictionary<TKey,TValue> элементу является и KeyValuePair<TKey, TValue> может быть деконструированы. Рассмотрим следующий пример.

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.");
}

Вы можете добавить метод в системные Deconstruct типы, у которых его нет. Рассмотрим следующий метод расширения:

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();
    }
}

Этот метод расширения позволяет деконструировать все Nullable<T> типы в кортеж (bool hasValue, T value). В следующем примере показан код, использующий этот метод расширения:

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 Типы

При объявлении типа record с помощью двух позиционных параметров или более компилятор создает метод Deconstruct с параметром out для каждого позиционного параметра в объявлении record. Дополнительные сведения см. в разделах Позиционный синтаксис для определения свойства и Поведение деконструктора в производных записях.

См. также