Поделиться через


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

Кортеж позволяет вам легко получить несколько значений при вызове метода. Но после получения кортежа вам нужно будет распорядиться его отдельными элементами. Работа с каждым элементом по отдельности может быть утомительной, как показано в следующем примере. Метод 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.
    }
    
  • Объявление и назначение переменных можно смешивать в деконструкции.

    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). Параметр в сигнатуре метода представляет каждое значение для деконструирования. Например, следующий метод 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. Первый возвращает набор значений, указывающих характеристики свойства. Второй указывает доступность свойства. Логические значения указывают, имеет ли свойство отдельные методы получения и задания доступа или разные специальные возможности. Если есть только один аксессор или если и get-аксессор, и set-аксессор имеют одинаковый уровень доступа, переменная access указывает на уровень доступа свойства в целом. В противном случае доступность аксессоров get и set указывается переменными getAccess и setAccess.

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

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

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

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

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

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

        public void Deconstruct(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 (propertyInfo.CanRead)
                getter = propertyInfo.GetMethod;

            MethodInfo setter = null;
            if (propertyInfo.CanWrite)
                setter = propertyInfo.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.");
}

record Типы

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

См. также