Деконструкция кортежей и других типовDeconstructing tuples and other types

Кортеж позволяет вам легко получить несколько значений при вызове метода.A tuple provides a lightweight way to retrieve multiple values from a method call. Но после получения кортежа вам нужно будет обработать его отдельные элементы.But once you retrieve the tuple, you have to handle its individual elements. Это довольно неудобно, как показано в следующем примере.Doing this on an element-by-element basis is cumbersome, as the following example shows. Метод QueryCityData возвращает кортеж из трех элементов, и каждый из его элементов присваивается переменной за отдельную операцию.The QueryCityData method returns a 3-tuple, and each of its elements is assigned to a variable in a separate operation.

using System;

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

Получение множества значений полей и свойств из объекта может быть столь же неудобно: необходимо присваивать значение каждого поля или свойства отдельной переменной.Retrieving multiple field and property values from an object can be equally cumbersome: you have to assign a field or property value to a variable on a member-by-member basis.

Начиная с C# 7.0 вы можете извлекать из кортежа множество элементов или получать множество значений полей, свойств и вычисляемых значений из объекта, используя всего одну операцию деконструкции.Starting with C# 7.0, you can retrieve multiple elements from a tuple or retrieve multiple field, property, and computed values from an object in a single deconstruct operation. При деконструкции кортежа вы присваиваете его элементы отдельным переменным.When you deconstruct a tuple, you assign its elements to individual variables. При деконструкции объекта вы присваиваете отдельным переменным выбранные значения.When you deconstruct an object, you assign selected values to individual variables.

Деконструкция кортежаDeconstructing a tuple

Язык C# имеет встроенную поддержку деконструкции кортежей, которая позволяет извлекать из кортежа все элементы за одну операцию.C# features built-in support for deconstructing tuples, which lets you unpackage all the items in a tuple in a single operation. Общий синтаксис деконструкции кортежа напоминает синтаксис его определения: переменные, которым будут присвоены элементы кортежа, указываются в круглых скобках в левой части оператора присваивания.The general syntax for deconstructing a tuple is similar to the syntax for defining one: you enclose the variables to which each element is to be assigned in parentheses in the left side of an assignment statement. Например, следующий оператор присваивает элементы кортежа из четырех элементов четырем отдельным переменным:For example, the following statement assigns the elements of a 4-tuple to four separate variables:

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

Существует три способа деконструкции кортежа:There are three ways to deconstruct a tuple:

  • Вы можете явно объявить тип каждого поля в скобках.You can explicitly declare the type of each field inside parentheses. В следующем примере этот способ используется для деконструкции кортежа из трех элементов, возвращаемого методом QueryCityData.The following example uses this approach to deconstruct the 3-tuple returned by the QueryCityData method.

    public static void Main()
    {
        (string city, int population, double area) = QueryCityData("New York City");
    
        // Do something with the data.
    }
    
  • Вы можете использовать ключевое слово var, чтобы C# определил тип каждой переменной.You can use the var keyword so that C# infers the type of each variable. Ключевое слово var помещается за пределами скобок.You place the var keyword outside of the parentheses. В следующем примере используется определение типа при деконструкции кортежа из трех элементов, возвращаемого методом QueryCityData.The following example uses type inference when deconstructing the 3-tuple returned by the QueryCityData method.

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

    Кроме того, вы можете использовать ключевое слово var при объявлении отдельных или всех переменных внутри скобок.You can also use the var keyword individually with any or all of the variable declarations inside the parentheses.

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

    Это неудобно и не рекомендуется.This is cumbersome and is not recommended.

  • Наконец, можно выполнить деконструкцию кортежа в переменные, которые уже были объявлены.Lastly, you may deconstruct the tuple into variables that have already been declared.

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

Обратите внимание, что вы не можете указать определенный тип за скобками, даже если все поля в кортеже имеют один и тот же тип.Note that you cannot specify a specific type outside the parentheses even if every field in the tuple has the same type. Это приведет к возникновению ошибки компилятора CS8136 "Форма деконструкции 'var (...)' не разрешает использовать конкретный тип для 'var'.".This generates compiler error CS8136, "Deconstruction 'var (...)' form disallows a specific type for 'var'.".

Обратите внимание, что каждый элемент кортежа необходимо присвоить переменной.Note that you must also assign each element of the tuple to a variable. Если вы пропустите какие-то элементы, компилятор выдаст сообщение об ошибке CS8132 "Невозможно деконструировать кортеж из "x" элементов на "y" переменных".If you omit any elements, the compiler generates error CS8132, "Cannot deconstruct a tuple of 'x' elements into 'y' variables."

Обратите внимание, что нельзя смешивать объявления и назначения существующим переменным в левой части деконструкции.Note that you cannot mix declarations and assignments to existing variables on the left-hand side of a deconstruction. Компилятор выводит ошибку CS8184: "Деконструкция не может содержать объявления и выражения в левой части",The compiler generates error CS8184, "a deconstruction cannot mix declarations and expressions on the left-hand-side." если члены содержат новые объявленные и существующие переменные.when the members include newly declared and existing variables.

Деконструкция элементов кортежа с использованием пустых переменныхDeconstructing tuple elements with discards

При деконструкции кортежа нас часто интересуют значения только некоторых элементов.Often when deconstructing a tuple, you're interested in the values of only some elements. Начиная с C# 7.0 вы можете воспользоваться поддержкой пустых переменных в C#, которые представляют собой доступные только для записи переменные, значения которых нас не интересуют.Starting with C# 7.0, you can take advantage of C#'s support for discards, which are write-only variables whose values you've chosen to ignore. Пустая переменная обозначается символом нижнего подчеркивания ("_") в операторе присваивания.A discard is designated by an underscore character ("_") in an assignment. Вы можете сделать пустыми сколько угодно значений. Все они будут считаться одной переменной, _.You can discard as many values as you like; all are represented by the single discard, _.

В следующем примере показано использование кортежей с пустыми переменными.The following example illustrates the use of tuples with discards. Метод QueryCityDataForYears возвращает кортеж из шести элементов: название города, его площадь, год, численность населения города в этом году, другой год и численность населения в том году.The QueryCityDataForYears method returns a 6-tuple with the name of a city, its area, a year, the city's population for that year, a second year, and the city's population for that second year. В примере показано изменение численности населения за эти два года.The example shows the change in population between those two years. Из доступных в кортеже данных нас не интересует площадь города, а название города и две даты известны нам уже на этапе разработки.Of the data available from the tuple, we're unconcerned with the city area, and we know the city name and the two dates at design-time. Следовательно, нас интересуют только два значения численности населения, которые хранятся в кортеже. Остальные значения можно обработать как пустые переменные.As a result, we're only interested in the two population values stored in the tuple, and can handle its remaining values as discards.

using System;
using System.Collections.Generic;

public class Example
{
    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

Деконструкция пользовательских типовDeconstructing user-defined types

C# не предоставляет встроенную поддержку для деконструкции типов, не являющихся кортежами.C# does not offer built-in support for deconstructing non-tuple types. Тем не менее, если вы являетесь создателем класса, структуры или интерфейса, вы можете разрешить деконструкцию экземпляров определенного типа, реализовав один или несколько методов Deconstruct.However, as the author of a class, a struct, or an interface, you can allow instances of the type to be deconstructed by implementing one or more Deconstruct methods. Метод возвращает "void", и каждое деконструируемое значение обозначается параметром out в сигнатуре метода.The method returns void, and each value to be deconstructed is indicated by an out parameter in the method signature. Например, следующий метод Deconstruct класса Person возвращает имя, отчество и фамилию:For example, the following Deconstruct method of a Person class returns the first, middle, and last name:

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

Затем вы можете выполнить деконструкцию экземпляра класса Person с именем p, используя подобное присваивание:You can then deconstruct an instance of the Person class named p with an assignment like the following:

var (fName, mName, lName) = p;

В следующем примере показана перегрузка метода Deconstruct для возвращения различных сочетаний свойств объекта Person.The following example overloads the Deconstruct method to return various combinations of properties of a Person object. Отдельные перегрузки возвращают следующие значения:Individual overloads return:

  • Имя и фамилия.A first and last name.
  • Имя, фамилия и отчество.A first, last, and middle name.
  • Имя, фамилия, название города и название штата.A first name, a last name, a city name, and a state name.
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 Example
{
    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 с уникальными и однозначными сигнатурами.Because you can overload the Deconstruct method to reflect groups of data that are commonly extracted from an object, you should be careful to define Deconstruct methods with signatures that are distinctive and unambiguous. Различные методы Deconstruct с одинаковым количеством параметров out или с одинаковым количеством и типом параметров out в разном порядке могут привести к путанице.Multiple Deconstruct methods that have the same number of out parameters or the same number and type of out parameters in a different order can cause confusion.

Перегруженный метод Deconstruct в следующем примере иллюстрирует один из источников возможной путаницы.The overloaded Deconstruct method in the following example illustrates one possible source of confusion. Первая перегрузка возвращает имя, отчество, фамилию и возраст объекта Person именно в таком порядке.The first overload returns the first name, middle name, last name, and age of a Person object, in that order. Вторая перегрузка возвращает только сведения об имени вместе с годовым доходом, но имя, отчество и фамилия стоят в другом порядке.The second overload returns name information only along with annual income, but the first, middle, and last name are in a different order. Это позволяет легко перепутать порядок аргументов при деконструкции экземпляра Person.This makes it easy to confuse the order of arguments when deconstructing a Person instance.

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 DateTime DateOfBirth { get; set; }
    public Decimal AnnualIncome { get; set; }

    public void Deconstruct(out string fname, out string mname, out string lname, out int age)
    {
        fname = FirstName;
        mname = MiddleName;
        lname = LastName;
        age = DateTime.Now.Year - DateOfBirth.Year;

        if (DateTime.Now.DayOfYear - (new DateTime(DateTime.Now.Year, DateOfBirth.Month, DateOfBirth.Day)).DayOfYear < 0)
            age--;
    }

    public void Deconstruct(out string lname, out string fname, out string mname, out decimal income)
    {
        fname = FirstName;
        mname = MiddleName;
        lname = LastName;
        income = AnnualIncome;
    }
}

Деконструкция пользовательского типа с пустыми переменнымиDeconstructing a user-defined type with discards

Как и с кортежами, пустые переменные можно применять с пользовательскими типами, чтобы игнорировать определенные элементы, возвращаемые методом Deconstruct.Just as you do with tuples, you can use discards to ignore selected items returned by a Deconstruct method. Каждая пустая переменная определяется переменной с именем "_", и одна операция деконструкции может включать несколько пустых переменных.Each discard is defined by a variable named "_", and a single deconstruction operation can include multiple discards.

В следующем примере показана деконструкция объекта Person на четыре строки (имя, фамилия, город и область), но для фамилии и области используются пустые переменные.The following example deconstructs a Person object into four strings (the first and last names, the city, and the state) but discards the last name and the state.

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

Деконструкция пользовательского типа с использованием метода расширенияDeconstructing a user-defined type with an extension method

Если вы не являетесь создателем класса, структуры или интерфейса, вы все равно можете выполнять деконструкцию объектов этого типа, реализовав один или несколько Deconstructметодов расширения, которые будут возвращать интересующие вас значения.If you didn't author a class, struct, or interface, you can still deconstruct objects of that type by implementing one or more Deconstruct extension methods to return the values in which you're interested.

В приведенном ниже примере определены два метода расширения Deconstruct для класса System.Reflection.PropertyInfo.The following example defines two Deconstruct extension methods for the System.Reflection.PropertyInfo class. Первый метод возвращает набор значений, которые указывают характеристики свойства, в том числе его тип, является ли оно статическим свойством или экземпляром, доступно ли оно только для чтения и является ли оно индексируемым.The first returns a set of values that indicate the characteristics of the property, including its type, whether it's static or instance, whether it's read-only, and whether it's indexed. Второй метод показывает уровень доступа свойства.The second indicates the property's accessibility. Так как методы доступа для чтения и записи у свойства могут иметь разный уровень доступа, мы используем логические значения, которые показывают, имеет ли свойство разные методы для чтения и записи и, если это так, имеют ли эти методы один уровень доступа.Because the accessibility of get and set accessors can differ, Boolean values indicate whether the property has separate get and set accessors and, if it does, whether they have the same accessibility. Если существует только один метод доступа или если методы доступа для чтения и записи имеют один и тот же уровень доступа, переменная access показывает доступность свойства в целом.If there is only one accessor or both the get and the set accessor have the same accessibility, the access variable indicates the accessibility of the property as a whole. В противном случае доступность методов чтения и записи указывается переменными getAccess и setAccess.Otherwise, the accessibility of the get and set accessors are indicated by the getAccess and setAccess variables.

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 Example
{
    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

См. такжеSee also