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

Кортеж позволяет вам легко получить несколько значений при вызове метода. Но после получения кортежа вам нужно будет обработать его отдельные элементы. Работа с элементом по элементу является громоздкой, как показано в следующем примере. Метод 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, "Деконструкция "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. Первый метод возвращает набор значений, которые указывают характеристики свойства, в том числе его тип, является ли оно статическим свойством или экземпляром, доступно ли оно только для чтения и является ли оно индексируемым. Второй метод показывает уровень доступа свойства. Так как методы доступа для чтения и записи у свойства могут иметь разный уровень доступа, мы используем логические значения, которые показывают, имеет ли свойство разные методы для чтения и записи и, если это так, имеют ли эти методы один уровень доступа. Если есть только один метод доступа или как метод получения, так и набор доступа имеют одинаковые специальные возможности, 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. Дополнительные сведения см. в разделах Позиционный синтаксис для определения свойства и Поведение деконструктора в производных записях.

См. также