Типы кортежей в C#C# tuple types

Кортежи в C# — эти типы, которые определяются с помощью упрощенного синтаксиса.C# tuples are types that you define using a lightweight syntax. Преимущества: более простой синтаксис, правила преобразований с учетом количества (так называемой кратности) и типов элементов, а также единые правила для копий, тестов на равенство и назначений.The advantages include a simpler syntax, rules for conversions based on number (referred to as cardinality) and types of elements, and consistent rules for copies, equality tests, and assignments. При этом кортежи не поддерживают некоторые объектно-ориентированные идиомы, связанные с наследованием.As a tradeoff, tuples do not support some of the object-oriented idioms associated with inheritance. Общие сведения см. в разделе "Кортежи" статьи Новые возможности C# 7.0.You can get an overview in the section on tuples in the What's new in C# 7.0 article.

В этой статье вы узнаете, какие правила языка регулируют кортежи в C# версии 7.0 и более поздних и каким образом их можно использовать, а также получите вводные рекомендации по работе с кортежами.In this article, you'll learn the language rules governing tuples in C# 7.0 and later versions, different ways to use them, and initial guidance on working with tuples.

Примечание

Для новых функций кортежа требуются типы ValueTuple.The new tuples features require the ValueTuple types. Для использования на платформах, которые не включают эти типы, необходимо добавить пакет NuGet System.ValueTuple.You must add the NuGet package System.ValueTuple in order to use it on platforms that do not include the types.

Это похоже на другие функции языка, использующие типы, предоставляемые на платформе.This is similar to other language features that rely on types delivered in the framework. В качестве примеров можно привести функции async и await, использующие интерфейс INotifyCompletion, а также LINQ на базе IEnumerable<T>.Examples include async and await relying on the INotifyCompletion interface, and LINQ relying on IEnumerable<T>. Тем не менее по мере увеличения степени независимости .NET от платформы механизм доставки изменяется.However, the delivery mechanism is changing as .NET is becoming more platform independent. Периодичность поставки новых выпусков .NET Framework не всегда совпадает с выпусками обновлений для компилятора языка.The .NET Framework may not always ship on the same cadence as the language compiler. Если новые функции языка используют новые типы, эти типы будут предоставляться в виде пакетов NuGet при выпуске таких новых функций.When new language features rely on new types, those types will be available as NuGet packages when the language features ship. Как только эти новые типы будут добавлены в стандартный API-интерфейс .NET и включены в состав платформы, требование обязательно использовать пакет NuGet будет снято.As these new types get added to the .NET Standard API and delivered as part of the framework, the NuGet package requirement will be removed.

Для начала обсудим, почему была добавлена поддержка кортежей.Let's start with the reasons for adding new tuple support. Методы возвращают один объект.Methods return a single object. Кортежи позволяют легко упаковать в этот объект сразу несколько значений.Tuples enable you to package multiple values in that single object more easily.

Платформа .NET Framework уже включает универсальные классы Tuple,The .NET Framework already has generic Tuple classes. которые, однако, имеют два серьезных ограничения.These classes, however, had two major limitations. Например, классы Tuple присваивают своим свойствам имена Item1, Item2 и т. д.For one, the Tuple classes named their properties Item1, Item2, and so on. Никакой семантической информации в этих именах нет.Those names carry no semantic information. Эти типы Tuple не позволяют сообщить пользователю значение каждого из свойств.Using these Tuple types does not enable communicating the meaning of each of the properties. Новые функции языка позволяют объявить и использовать семантически значимые имена для элементов в кортеже.The new language features enable you to declare and use semantically meaningful names for the elements in a tuple.

Классы Tuple вызывают несколько проблем с производительностью, так как они имеют ссылочный тип.The Tuple classes cause more performance concerns because they are reference types. При использовании типов Tuple происходит распределение объектов.Using one of the Tuple types means allocating objects. В критических путях выделение множества небольших объектов может заметно влиять на производительность приложения.On hot paths, allocating many small objects can have a measurable impact on your application's performance. Следовательно, при поддержке языков для кортежей используются новые структуры ValueTuple.Therefore, the language support for tuples leverages the new ValueTuple structs.

Чтобы избежать этих недостатков, можно создать class или struct, включающие несколько элементов.To avoid those deficiencies, you could create a class or a struct to carry multiple elements. К сожалению, это означает дополнительную работу и искажает цель вашего проекта.Unfortunately, that's more work for you, and it obscures your design intent. Создание struct или class означает, что определение типа включает и данные, и поведение.Making a struct or class implies that you are defining a type with both data and behavior. Во многих случаях все, что вам нужно, — это сохранить в одном объекте несколько значений.Many times, you simply want to store multiple values in a single object.

Возможности языка и универсальные структуры ValueTuple обеспечивают соблюдение правила, запрещающего добавлять поведение (методы) в эти типы кортежей.The language features and the ValueTuple generic structs enforce the rule that you cannot add any behavior (methods) to these tuple types. Все типы ValueTuple представляют собой изменяемые структуры.All the ValueTuple types are mutable structs. Все поля членов открыты,Each member field is a public field. что делает их очень простыми.That makes them very lightweight. В то же время это означает, что кортежи не стоит использовать в случаях, когда важна неизменность.However, that means tuples should not be used where immutability is important.

Кортежи — это более простые и более гибкие контейнеры данных, чем типы class и struct.Tuples are both simpler and more flexible data containers than class and struct types. Рассмотрим эти различия.Let's explore those differences.

Именованные и неименованные кортежиNamed and unnamed tuples

Структура ValueTuple включает поля с именами Item1, Item2, Item3 и т. д., аналогичные свойствам, определенным в существующих типах Tuple.The ValueTuple struct has fields named Item1, Item2, Item3, and so on, similar to the properties defined in the existing Tuple types. Только эти имена можно использовать для неименованных кортежей.These names are the only names you can use for unnamed tuples. Если никакие альтернативные имена полей кортежу не предоставлены, значит, вы создали неименованный кортеж:When you do not provide any alternative field names to a tuple, you've created an unnamed tuple:

var unnamed = ("one", "two");

Кортеж в предыдущем примере был инициализирован с помощью констант-литералов, и в нем не будут созданы имена элементов с помощью проекций имен полей кортежа на C# 7.1.The tuple in the previous example was initialized using literal constants and won't have element names created using tuple field name projections in C# 7.1.

В то же время при инициализации кортежа можно использовать новые возможности языка, позволяющие присваивать всем полям более понятные имена.However, when you initialize a tuple, you can use new language features that give better names to each field. При этом создается именованный кортеж.Doing so creates a named tuple. Именованные кортежи тоже содержат имена с элементами Item1, Item2, Item3 и т. д.Named tuples still have elements named Item1, Item2, Item3 and so on. В то же время у них есть синонимы для всех элементов, которым вы присвоили имена.But they also have synonyms for any of those elements that you have named. Для создания именованного кортежа необходимо указать имя для каждого элемента.You create a named tuple by specifying the names for each element. Один из способов — указать имена в процессе инициализации кортежа:One way is to specify the names as part of the tuple initialization:

var named = (first: "one", second: "two");

Эти синонимы обрабатываются компилятором и языком, что позволяет эффективно использовать именованные кортежи.These synonyms are handled by the compiler and the language so that you can use named tuples effectively. Интегрированные среды разработки и редакторы читают эти семантические имена с помощью API Roslyn.IDEs and editors can read these semantic names using the Roslyn APIs. Вы можете ссылаться на элементы именованного кортежа по семантическим именам в любой части сборки.You can reference the elements of a named tuple by those semantic names anywhere in the same assembly. При создании компилированных выходных данных компилятор заменяет имена, определенные с помощью эквивалентов Item*.The compiler replaces the names you've defined with Item* equivalents when generating the compiled output. Скомпилированный MSIL не включает имена, которые вы присвоили этим элементам.The compiled Microsoft Intermediate Language (MSIL) does not include the names you've given these elements.

Начиная с C# 7.1, имена полей для кортежа могут быть получены из переменных, которые используются для его инициализации.Beginning with C# 7.1, the field names for a tuple may be provided from the variables used to initialize the tuple. Они называются инициализаторами проекций кортежа.This is referred to as tuple projection initializers. Следующий код создает кортеж accumulation с элементами count (целое значение) и sum (значение double).The following code creates a tuple named accumulation with elements count (an integer), and sum (a double).

var sum = 12.5;
var count = 5;
var accumulation = (count, sum);

Компилятор должен передавать созданные вами имена для кортежей, возвращаемых открытыми методами и свойствами.The compiler must communicate those names you created for tuples that are returned from public methods or properties. В подобных случаях компилятор добавляет к методу атрибут TupleElementNamesAttribute.In those cases, the compiler adds a TupleElementNamesAttribute attribute on the method. Этот атрибут содержит свойство списка TransformNames, которое содержит имена, присвоенные каждому элементу в кортеже.This attribute contains a TransformNames list property that contains the names given to each of the elements in the tuple.

Примечание

Средства разработки, такие как Visual Studio, также считывают эти метаданные и предоставляют IntelliSense и другие возможности, используя имена полей метаданных.Development Tools, such as Visual Studio, also read that metadata, and provide IntelliSense and other features using the metadata field names.

Знать основы работы с новыми кортежами и типом ValueTuple необходимо для того, чтобы понимать правила назначения именованных кортежей друг другу.It is important to understand these underlying fundamentals of the new tuples and the ValueTuple type in order to understand the rules for assigning named tuples to each other.

Инициализаторы проекций кортежаTuple projection initializers

Как правило, инициализаторы проекций кортежа работают с использованием переменной или имен полей в правой части оператора инициализации кортежа.In general, tuple projection initializers work by using the variable or field names from the right-hand side of a tuple initialization statement. Если задано явное имя, оно имеет приоритет над именем проекции.If an explicit name is given, that takes precedence over any projected name. Например, в следующем инициализаторе элементы имеют значения explicitFieldOne и explicitFieldTwo, а не localVariableOne и localVariableTwo:For example, in the following initializer, the elements are explicitFieldOne and explicitFieldTwo, not localVariableOne and localVariableTwo:

var localVariableOne = 5;
var localVariableTwo = "some text";

var tuple = (explicitFieldOne: localVariableOne, explicitFieldTwo: localVariableTwo);

Во всех полях, где не указано явное имя, проецируется применимое неявное имя.For any field where an explicit name is not provided, an applicable implicit name is projected. Необязательно указывать семантические имена как явно, так и неявно.There is no requirement to provide semantic names, either explicitly or implicitly. Следующий инициализатор имеет имена полей Item1 со значением 42 и stringContent ("Ответ на все вопросы"):The following initializer has field names Item1, whose value is 42 and stringContent, whose value is "The answer to everything":

var stringContent = "The answer to everything";
var mixedTuple = (42, stringContent);

Существуют два условия, при которых имена полей кандидата не проецируются в поле кортежа.There are two conditions where candidate field names are not projected onto the tuple field:

  1. Имя кандидата является зарезервированным именем кортежа.When the candidate name is a reserved tuple name. К примерам относятся Item3, ToStringExamples include Item3, ToString. и Rest.or Rest.
  2. Имя кандидата является дубликатом другого имени поля кортежа, явного или неявного.When the candidate name is a duplicate of another tuple field name, either explicit or implicit.

Эти условия позволяют избежать неоднозначности.These conditions avoid ambiguity. Эти имена вызывали бы неоднозначность, если бы использовались в качестве имен полей для поля в кортеже.These names would cause an ambiguity if they were used as the field names for a field in a tuple. Ни одно из этих условий не вызовет ошибки времени компиляции.Neither of these conditions cause compile-time errors. Семантические имена элементов без проецируемых имен не будут проецироваться.Instead, the elements without projected names do not have semantic names projected for them. Все это показано в приведенных ниже примерах.The following examples demonstrate these conditions:

var ToString = "This is some text";
var one = 1;
var Item1 = 5;
var projections = (ToString, one, Item1);
// Accessing the first field:
Console.WriteLine(projections.Item1);
// There is no semantic name 'ToString'
// Accessing the second field:
Console.WriteLine(projections.one);
Console.WriteLine(projections.Item2);
// Accessing the third field:
Console.WriteLine(projections.Item3);
// There is no semantic name 'Item1`.

var pt1 = (X: 3, Y: 0);
var pt2 = (X: 3, Y: 4);

var xCoords = (pt1.X, pt2.X);
// There are no semantic names for the fields
// of xCoords. 

// Accessing the first field:
Console.WriteLine(xCoords.Item1);
// Accessing the second field:
Console.WriteLine(xCoords.Item2);

Эти ситуации не вызывают ошибок компилятора, так как это стало бы критическим изменением кода, написанного на C# 7.0, где проекции имен полей кортежа были недоступны.These situations do not cause compiler errors because that would be a breaking change for code written with C# 7.0, when tuple field name projections were not available.

Равенство и кортежиEquality and tuples

Начиная с C# 7.3, типы кортежей поддерживают операторы == и !=.Beginning with C# 7.3, tuple types support the == and != operators. Эти операторы работают путем сравнения каждого элемента левого аргумента с каждым элементом правого аргумента по порядку.These operators work by comparing each member of the left argument to each member of the right argument in order. Это сокращенные сравнения.These comparisons short-circuit. Они перестанут сравнивать элементы, как только будет обнаружена неравная пара.They will stop evaluating members as soon as one pair is not equal. В следующем примере кода используется оператор ==, но все правила сравнения применяются к оператору !=.The following code examples use ==, but the comparison rules all apply to !=. В следующем примере кода показано сравнение двух пар целых чисел на равенство:The following code example shows an equality comparison for two pairs of integers:

var left = (a: 5, b: 10);
var right = (a: 5, b: 10);
Console.WriteLine(left == right); // displays 'true'

Существует несколько правил, которые упрощают тесты на равенство кортежей.There are several rules that make tuple equality tests more convenient. Если один из кортежей допускает значение NULL, как показано в следующем коде, функция проверки кортежей на равенство выполняет преобразования для использования форм, допускающих значение NULL:Tuple equality performs lifted conversions if one of the tuples is a nullable tuple, as shown in the following code:

var left = (a: 5, b: 10);
var right = (a: 5, b: 10);
(int a, int b)? nullableTuple = right;
Console.WriteLine(left == nullableTuple); // Also true

Функция проверки кортежей на равенство также выполняет неявное преобразование каждого элемента обоих кортежей.Tuple equality also performs implicit conversions on each member of both tuples. К ним относятся преобразования для использования форм, допускающих значение NULL, расширяющие преобразования и другие неявные преобразования.These include lifted conversions, widening conversions, or other implicit conversions. В следующих примерах показано, что кортеж из 2 элементов типа "целое число" можно сравнить с кортежем из 2 элементов типа "длинное целое" в связи с неявным преобразованием из целого числа в длинное целое:The following examples show that an integer 2-tuple can be compared to a long 2-tuple because of the implicit conversion from integer to long:

// lifted conversions
var left = (a: 5, b: 10);
(int? a, int? b) nullableMembers = (5, 10);
Console.WriteLine(left == nullableMembers); // Also true

// converted type of left is (long, long)
(long a, long b) longTuple = (5, 10);
Console.WriteLine(left == longTuple); // Also true

// comparisons performed on (long, long) tuples
(long a, int b) longFirst = (5, 10);
(int a, long b) longSecond = (5, 10);
Console.WriteLine(longFirst == longSecond); // Also true

Имена элементов кортежей не участвуют в тестах на равенство.The names of the tuple members do not participate in tests for equality. Тем не менее если один из операндов является литералом кортежа с явными именами, компилятор генерирует предупреждение CS8383 в случае несовпадения этих имен с именами второго операнда.However, if one of the operands is a tuple literal with explicit names, the compiler generates warning CS8383 if those names do not match the names of the other operand. В случае, когда оба операнда являются литералами кортежей, предупреждение находится возле правого операнда, как показано в следующем примере:In the case where both operands are tuple literals, the warning is on the right operand as shown in the following example:

(int a, string b) pair = (1, "Hello");
(int z, string y) another = (1, "Hello");
Console.WriteLine(pair == another); // true. Member names don't participate.
Console.WriteLine(pair == (z: 1, y: "Hello")); // warning: literal contains different member names

Наконец, кортежи могут содержать вложенные кортежи.Finally, tuples may contain nested tuples. Функция проверки кортежей на равенство сравнивает "форму" каждого операнда по вложенным кортежам, как показано в следующем примере:Tuple equality compares the "shape" of each operand through nested tuples as shown in the following example:

(int, (int, int)) nestedTuple = (1, (2, 3));
Console.WriteLine(nestedTuple == (1, (2, 3)) );

Это ошибка времени компиляции для сравнения двух кортежей на равенство (или неравенство) при наличии разных фигур.It's a compile time error to compare two tuples for equality (or inequality) when they have different shapes. Компилятор не пытается выполнить деконструкцию вложенных кортежей, чтобы сравнить их.The compiler won't attempt any deconstruction of nested tuples in order to compare them.

Назначение и кортежиAssignment and tuples

Язык поддерживает назначение между типами кортежей с одинаковым количеством элементов, где каждый расположенный справа элемент может быть неявно преобразован в соответствующий элемент, расположенный слева.The language supports assignment between tuple types that have the same number of elements, where each right-hand side element can be implicitly converted to its corresponding left-hand side element. Другие преобразования в контексте назначений не учитываются.Other conversions aren't considered for assignments. Это ошибки времени компиляции для назначения одного кортежа другому при наличии различных фигур.It's a compile time error to assign one tuple to another when they have different shapes. Компилятор не будет пытаться выполнить любую деконструкцию вложенных кортежей для их назначения.The compiler won't attempt any deconstruction of nested tuples in order to assign them. Рассмотрим возможные виды назначений между типами кортежей.Let's look at the kinds of assignments that are allowed between tuple types.

В приведенных ниже примерах можно использовать указанные переменные:Consider these variables used in the following examples:

// The 'arity' and 'shape' of all these tuples are compatible. 
// The only difference is the field names being used.
var unnamed = (42, "The meaning of life");
var anonymous = (16, "a perfect square");
var named = (Answer: 42, Message: "The meaning of life");
var differentNamed = (SecretConstant: 42, Label: "The meaning of life");

В первых двух переменных, unnamed и anonymous, семантические имена элементам не назначены.The first two variables, unnamed and anonymous do not have semantic names provided for the elements. Имена полей — Item1 и Item2.The field names are Item1 and Item2. Последние две переменные, named и differentName, включают назначенные элементам семантические имена.The last two variables, named and differentName have semantic names given for the elements. Элементы в этих двух кортежах называются по-разному.These two tuples have different names for the elements.

Все четыре этих кортежа имеют одинаковое число элементов (так называемую кратность), а типы этих элементов идентичны.All four of these tuples have the same number of elements (referred to as 'cardinality') and the types of those elements are identical. Таким образом, все эти назначения работают:Therefore, all of these assignments work:

unnamed = named;

named = unnamed;
// 'named' still has fields that can be referred to
// as 'answer', and 'message':
Console.WriteLine($"{named.Answer}, {named.Message}");

// unnamed to unnamed:
anonymous = unnamed;

// named tuples.
named = differentNamed;
// The field names are not assigned. 'named' still has 
// fields that can be referred to as 'answer' and 'message':
Console.WriteLine($"{named.Answer}, {named.Message}");

// With implicit conversions:
// int can be implicitly converted to long
(long, string) conversion = named;

Обратите внимание на то, что имена кортежей не назначаются.Notice that the names of the tuples are not assigned. Значения элементов назначаются в соответствии с порядком элементов в кортеже.The values of the elements are assigned following the order of the elements in the tuple.

Кортежи различных типов или с различным количеством элементов не назначаются.Tuples of different types or numbers of elements are not assignable:

// Does not compile.
// CS0029: Cannot assign Tuple(int,int,int) to Tuple(int, string)
var differentShape = (1, 2, 3);
named = differentShape;

Кортежи как возвращаемые значения методовTuples as method return values

Чаще всего кортежи используются как возвращаемое методом значение.One of the most common uses for tuples is as a method return value. Разберем конкретный пример.Let's walk through one example. Рассмотрим метод, который вычисляет стандартное отклонение для последовательности чисел:Consider this method that computes the standard deviation for a sequence of numbers:

public static double StandardDeviation(IEnumerable<double> sequence)
{
    // Step 1: Compute the Mean:
    var mean = sequence.Average();

    // Step 2: Compute the square of the differences between each number 
    // and the mean:
    var squaredMeanDifferences = from n in sequence
                                 select (n - mean) * (n - mean);
    // Step 3: Find the mean of those squared differences:
    var meanOfSquaredDifferences = squaredMeanDifferences.Average();

    // Step 4: Standard Deviation is the square root of that mean:
    var standardDeviation = Math.Sqrt(meanOfSquaredDifferences);
    return standardDeviation;
}

Примечание

Код в этих примерах вычисляет стандартное отклонение неисправленной выборки.These examples compute the uncorrected sample standard deviation. Формула стандартного отклонения исправленной выборки делит сумму квадратов разности со средним значением на (N-1), как и метод расширения Average.The corrected sample standard deviation formula would divide the sum of the squared differences from the mean by (N-1) instead of N, as the Average extension method does. Дополнительные сведения о различиях между этими формулами для расчета стандартного отклонения см. в тексте по статистике.Consult a statistics text for more details on the differences between these formulas for standard deviation.

Приведенный выше код соответствует учебной формуле для стандартного отклонения.The preceding code follows the textbook formula for the standard deviation. Она позволяет получить правильный ответ, однако эта реализация неэффективна.It produces the correct answer, but it's an inefficient implementation. Этот метод перечисляет последовательность дважды. Один раз для получения среднего значения, а второй — для получения среднего значения квадратов разницы со средним.This method enumerates the sequence twice: Once to produce the average, and once to produce the average of the square of the difference of the average. (Запомните, что запросы LINQ вычисляются в отложенном режиме, поэтому разница со средним значением и среднее этих разниц вычисляются в один прием.)(Remember that LINQ queries are evaluated lazily, so the computation of the differences from the mean and the average of those differences makes only one enumeration.)

Существует альтернативная формула, которая вычисляет стандартное отклонение, используя только одно перечисление последовательности.There is an alternative formula that computes standard deviation using only one enumeration of the sequence. В результате этого вычисления выдаются два значения, поскольку оно перечисляет последовательность: сумма всех элементов в последовательности и сумма квадратов всех значений:This computation produces two values as it enumerates the sequence: the sum of all items in the sequence, and the sum of the each value squared:

public static double StandardDeviation(IEnumerable<double> sequence)
{
    double sum = 0;
    double sumOfSquares = 0;
    double count = 0;

    foreach (var item in sequence)
    {
        count++;
        sum += item;
        sumOfSquares += item * item;
    }

    var variance = sumOfSquares - sum * sum / count;
    return Math.Sqrt(variance / count);
}

Эта версия перечисляет последовательность ровно один раз.This version enumerates the sequence exactly once. В то же время этот код нельзя повторно использовать.But it's not reusable code. По мере работы вы узнаете, что во многих статистических вычислениях используется число элементов в последовательности, сумма последовательности и сумма квадратов последовательности.As you keep working, you'll find that many different statistical computations use the number of items in the sequence, the sum of the sequence, and the sum of the squares of the sequence. Выполним рефакторинг этого метода и напишем служебный метод, выдающий все три из этих значений.Let's refactor this method and write a utility method that produces all three of those values. Все три значения могут быть возвращены в виде кортежа.All three values can be returned as a tuple.

Обновим этот метод таким образом, чтобы все три значения, вычисляемые при перечислении, сохранялись в кортеж.Let's update this method so the three values computed during the enumeration are stored in a tuple. В результате создается следующая версия:That creates this version:

public static double StandardDeviation(IEnumerable<double> sequence)
{
    var computation = (Count: 0, Sum: 0.0, SumOfSquares: 0.0);

    foreach (var item in sequence)
    {
        computation.Count++;
        computation.Sum += item;
        computation.SumOfSquares += item * item;
    }

    var variance = computation.SumOfSquares - computation.Sum * computation.Sum / computation.Count;
    return Math.Sqrt(variance / computation.Count);
}

Поддержка рефакторинга в Visual Studio позволяет легко извлекать функции основной статистики в закрытый метод.Visual Studio's Refactoring support makes it easy to extract the functionality for the core statistics into a private method. В результате вы получаете метод private static, возвращающий тип кортежа с тремя значениями — Sum, SumOfSquares и Count:That gives you a private static method that returns the tuple type with the three values of Sum, SumOfSquares, and Count:

public static double StandardDeviation(IEnumerable<double> sequence)
{
    (int Count, double Sum, double SumOfSquares) computation = ComputeSumAndSumOfSquares(sequence);

    var variance = computation.SumOfSquares - computation.Sum * computation.Sum / computation.Count;
    return Math.Sqrt(variance / computation.Count);
}

private static (int Count, double Sum, double SumOfSquares) ComputeSumAndSumOfSquares(IEnumerable<double> sequence)
{
    var computation = (count: 0, sum: 0.0, sumOfSquares: 0.0);

    foreach (var item in sequence)
    {
        computation.count++;
        computation.sum += item;
        computation.sumOfSquares += item * item;
    }

    return computation;
}

Язык позволяет использовать несколько дополнительных параметров, с помощью которых можно внести несколько быстрых изменений вручную.The language enables a couple more options that you can use, if you want to make a few quick edits by hand. Во-первых, объявление var можно использовать для инициализации результата кортежа из вызова метода ComputeSumAndSumOfSquares.First, you can use the var declaration to initialize the tuple result from the ComputeSumAndSumOfSquares method call. Кроме того, в методе ComputeSumAndSumOfSquares можно создать три дискретные переменные.You can also create three discrete variables inside the ComputeSumAndSumOfSquares method. Итоговая версия показана в следующем коде:The final version is shown in the following code:

public static double StandardDeviation(IEnumerable<double> sequence)
{
    var computation = ComputeSumAndSumOfSquares(sequence);

    var variance = computation.SumOfSquares - computation.Sum * computation.Sum / computation.Count;
    return Math.Sqrt(variance / computation.Count);
}

private static (int Count, double Sum, double SumOfSquares) ComputeSumAndSumOfSquares(IEnumerable<double> sequence)
{
    double sum = 0;
    double sumOfSquares = 0;
    int count = 0;

    foreach (var item in sequence)
    {
        count++;
        sum += item;
        sumOfSquares += item * item;
    }

    return (count, sum, sumOfSquares);
}

Окончательную версию можно применять к любому методу, которому требуются эти три значения, а также к любому их подмножеству.This final version can be used for any method that needs those three values, or any subset of them.

Язык поддерживает другие варианты управления именами элементов в методах, возвращающих кортежи.The language supports other options in managing the names of the elements in these tuple-returning methods.

Имена полей можно удалить из объявления возвращаемого значения и вернуть неименованный кортеж:You can remove the field names from the return value declaration and return an unnamed tuple:

private static (double, double, int) ComputeSumAndSumOfSquares(IEnumerable<double> sequence)
{
    double sum = 0;
    double sumOfSquares = 0;
    int count = 0;

    foreach (var item in sequence)
    {
        count++;
        sum += item;
        sumOfSquares += item * item;
    }

    return (sum, sumOfSquares, count);
}

Поля этого кортежа называются Item1, Item2 и Item3.The fields of this tuple are named Item1, Item2, and Item3. Для элементов кортежей, возвращаемых из методов, рекомендуется указывать семантические имена.It's recommended that you provide semantic names to the elements of tuples returned from methods.

Еще одна идиома, в которой кортежи могут быть полезны, связана с созданием запросов LINQ.Another idiom where tuples can be useful is when you are authoring LINQ queries. Итоговый проецируемый результат часто содержит некоторые, но не все свойства выбранных объектов.The final projected result often contains some, but not all, of the properties of the objects being selected.

Обычно результаты запроса проецируются в последовательность объектов анонимного типа.You would traditionally project the results of the query into a sequence of objects that were an anonymous type. С этим связано множество ограничений, в первую очередь потому, что анонимным типам нельзя присваивать удобные имена в возвращаемом типе для метода.That presented many limitations, primarily because anonymous types could not conveniently be named in the return type for a method. Альтернативные варианты с типом результата object или dynamic вызывают серьезные потери в производительности.Alternatives using object or dynamic as the type of the result came with significant performance costs.

Вернуть последовательность с типом кортежа легко, а имена и типы элементов можно получить во время компиляции и с помощью средств IDE.Returning a sequence of a tuple type is easy, and the names and types of the elements are available at compile time and through IDE tools. В качестве примера рассмотрим приложение ToDo.For example, consider a ToDo application. Определите класс следующего вида — он будет представлять одну запись в списке дел ToDo:You might define a class similar to the following to represent a single entry in the ToDo list:

public class ToDoItem
{
    public int ID { get; set; }
    public bool IsDone { get; set; }
    public DateTime DueDate { get; set; }
    public string Title { get; set; }
    public string Notes { get; set; }    
}

Мобильные приложения могут поддерживать компактную форму текущих пунктов в списке дел, когда отображается только заголовок.Your mobile applications may support a compact form of the current ToDo items that only displays the title. Этот запрос LINQ выполняет проекцию, включающую только идентификатор и заголовок.That LINQ query would make a projection that includes only the ID and the title. Эту модель хорошо демонстрирует метод, возвращающий последовательность кортежей:A method that returns a sequence of tuples expresses that design well:

internal IEnumerable<(int ID, string Title)> GetCurrentItemsMobileList()
{
    return from item in AllItems
           where !item.IsDone
           orderby item.DueDate
           select (item.ID, item.Title);
}

Примечание

В C# 7.1 проекции кортежей позволяют создавать именованные кортежи с помощью элементов аналогично именованию свойств в анонимных типах.In C# 7.1, tuple projections enable you to create named tuples using elements, in a manner similar to the property naming in anonymous types. В коде выше оператор select в проекции запросов создает кортеж с элементами ID и Title.In the above code, the select statement in the query projection creates a tuple that has elements ID and Title.

Именованный кортеж может быть частью сигнатуры.The named tuple can be part of the signature. Это позволяет компилятору и средствам IDE статически проверять, правильно ли используются результаты.It lets the compiler and IDE tools provide static checking that you are using the result correctly. Именованный кортеж также несет в себе данные статического типа, избавляя от необходимости использовать для работы с результатами дорогостоящие функции среды выполнения, такие как отражение или динамическая привязка.The named tuple also carries the static type information so there is no need to use expensive run time features like reflection or dynamic binding to work with the results.

ДеконструкцияDeconstruction

Все элементы в кортеже можно распаковать, выполнив деконструкцию возвращаемого методом кортежа.You can unpackage all the items in a tuple by deconstructing the tuple returned by a method. Деконструкцию кортежей можно выполнять тремя различными способами.There are three different approaches to deconstructing tuples. Во-первых, можно явно объявить тип каждого поля, заключив его в круглые скобки, чтобы создать дискретные переменные для каждого элемента в кортеже:First, you can explicitly declare the type of each field inside parentheses to create discrete variables for each of the elements in the tuple:

public static double StandardDeviation(IEnumerable<double> sequence)
{
    (int count, double sum, double sumOfSquares) = ComputeSumAndSumOfSquares(sequence);

    var variance = sumOfSquares - sum * sum / count;
    return Math.Sqrt(variance / count);
}

Типизированные переменные для каждого поля в кортеже можно также объявить неявно, используя ключевое слово var за скобками:You can also declare implicitly typed variables for each field in a tuple by using the var keyword outside the parentheses:

public static double StandardDeviation(IEnumerable<double> sequence)
{
    var (sum, sumOfSquares, count) = ComputeSumAndSumOfSquares(sequence);

    var variance = sumOfSquares - sum * sum / count;
    return Math.Sqrt(variance / count);
}

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

(double sum, var sumOfSquares, var count) = ComputeSumAndSumOfSquares(sequence);

Определенный тип использовать за скобками нельзя, даже если каждое поле в кортеже имеет одинаковый тип.You cannot use a specific type outside the parentheses, even if every field in the tuple has the same type.

Можно также выполнить деконструкцию кортежей с существующими объявлениями:You can deconstruct tuples with existing declarations as well:

public class Point
{
    public int X { get; set; }
    public int Y { get; set; }

    public Point(int x, int y) => (X, Y) = (x, y);
}

Предупреждение

Нельзя смешивать существующие объявления с объявлениями в круглых скобках.You cannot mix existing declarations with declarations inside the parentheses. Например, следующее выражение является недопустимым: (var x, y) = MyMethod();.For instance, the following is not allowed: (var x, y) = MyMethod();. В результате возникает ошибка CS8184, так как переменная x объявлена внутри скобок, а переменная y объявлена ранее в другом месте.This produces error CS8184 because x is declared inside the parentheses and y is previously declared elsewhere.

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

Любой тип кортежа можно деконструировать, как показано выше.Any tuple type can be deconstructed as shown above. Деконструкцию можно легко активировать для любого пользовательского типа (классов, структур и даже интерфейсов).It's also easy to enable deconstruction on any user-defined type (classes, structs, or even interfaces).

Автор типа может определить один или несколько методов Deconstruct, присваивающих значения любому количеству переменных out, которые представляют составляющие этот тип элементы данных.The type author can define one or more Deconstruct methods that assign values to any number of out variables representing the data elements that make up the type. Например, следующий тип Person определяет метод Deconstruct, который деконструирует объект person в элементы, представляющие имя и фамилию:For example, the following Person type defines a Deconstruct method that deconstructs a person object into the elements representing the first name and last name:

public class Person
{
    public string FirstName { get; }
    public string LastName { get; }

    public Person(string first, string last)
    {
        FirstName = first;
        LastName = last;
    }

    public void Deconstruct(out string firstName, out string lastName)
    {
        firstName = FirstName;
        lastName = LastName;
    }
}

Метод deconstruct позволяет выполнять назначение из объекта Person в две строки, представляющие свойства FirstName и LastName:The deconstruct method enables assignment from a Person to two strings, representing the FirstName and LastName properties:

var p = new Person("Althea", "Goodwin");
var (first, last) = p;

Вы можете включить деконструкцию даже для типов, которые не создавали.You can enable deconstruction even for types you did not author. Метод Deconstruct может быть методом расширения, который распаковывает доступные элементы данных в объекте.The Deconstruct method can be an extension method that unpackages the accessible data members of an object. В следующем примере показан тип Student, производный от типа Person, и метод расширения, который разбивает Student на три переменные, представляющие FirstName, LastName и GPA:The example below shows a Student type, derived from the Person type, and an extension method that deconstructs a Student into three variables, representing the FirstName, the LastName, and the GPA:

public class Student : Person
{
    public double GPA { get; }
    public Student(string first, string last, double gpa) :
        base(first, last)
    {
        GPA = gpa;
    }
}

public static class Extensions
{
    public static void Deconstruct(this Student s, out string first, out string last, out double gpa)
    {
        first = s.FirstName;
        last = s.LastName;
        gpa = s.GPA;
    }
}

Теперь у объекта Student есть два доступных метода Deconstruct: метод расширения, объявленный для типов Student, и элемент типа Person.A Student object now has two accessible Deconstruct methods: the extension method declared for Student types, and the member of the Person type. Оба из них входят в область, что позволяет разбить Student либо на две, либо на три переменные.Both are in scope, and that enables a Student to be deconstructed into either two variables or three. Если учащийся назначается трем переменным, возвращается все — имя, фамилия и GPA.If you assign a student to three variables, the first name, last name, and GPA are all returned. Если учащийся назначается двум переменным, возвращаются только имя и фамилия.If you assign a student to two variables, only the first name and the last name are returned.

var s1 = new Student("Cary", "Totten", 4.5);
var (fName, lName, gpa) = s1;

Определять несколько методов Deconstruct в классе или иерархии классов необходимо с осторожностью.You should be careful defining multiple Deconstruct methods in a class or a class hierarchy. Несколько методов Deconstruct с одинаковым числом параметров out могут быстро вызвать неоднозначность.Multiple Deconstruct methods that have the same number of out parameters can quickly cause ambiguities. В итоге вызывающие объекты могут лишиться возможности вызвать желаемый метод Deconstruct.Callers may not be able to easily call the desired Deconstruct method.

В этом примере вероятность неоднозначного вызова минимальна, поскольку метод Deconstruct для Person имеет два параметра вывода, а метод Deconstruct для Student — три.In this example, there is minimal chance for an ambiguous call because the Deconstruct method for Person has two output parameters, and the Deconstruct method for Student has three.

Операторы деконструкции не участвуют в проверке равенства.Deconstruction operators do not participate in testing equality. Следующий пример создает ошибку компилятора CS0019.The following example generates compiler error CS0019:

Person p = new Person("Althea", "Goodwin");
if (("Althea", "Goodwin") == p)
    Console.WriteLine(p);

Метод Deconstruct может преобразовать объект Person p в кортеж, содержащий две строки, но он неприменим в контексте проверок на равенство.The Deconstruct method could convert the Person object p to a tuple containing two strings, but it is not applicable in the context of equality tests.

ЗаключениеConclusion

Добавленная в язык и библиотеку поддержка именованных кортежей значительно упрощает работу с проектами, в которых используются структуры данных, хранящие несколько элементов, но не определяющие поведение подобно классам или структурам.The new language and library support for named tuples makes it much easier to work with designs that use data structures that store multiple elements but do not define behavior, as classes and structs do. Для этих типов удобно использовать кортежи.It's easy and concise to use tuples for those types. Они дают вам все преимущества статической поддержки типов и в то же время избавляют от необходимости создавать типы с использованием более подробного синтаксиса class или struct.You get all the benefits of static type checking, without needing to author types using the more verbose class or struct syntax. И все же наибольшую пользу они приносят при использовании со служебными методами, имеющими атрибут private или internal.Even so, they are most useful for utility methods that are private, or internal. Создавайте пользовательские типы, class или struct, если ваши открытые методы возвращают значение с несколькими элементами.Create user-defined types, either class or struct types when your public methods return a value that has multiple elements.