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


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

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

record Типы

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

См. также