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.ValueTupleYou 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. 例如,依赖 INotifyCompletion 接口的 asyncawait,依赖 IEnumerable<T> 的 LINQ。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. 这些新类型添加到 .NET 标准 API 并作为框架的一部分交付后,将删除 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 类将其属性命名为 Item1Item2 等。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.

为避免这些缺陷,可创建 classstruct 来承载多个元素。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. 创建 structclass 意味着定义一个具有数据和行为的类型。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.

元组是比 classstruct 类型更为简单灵活的数据容器。Tuples are both simpler and more flexible data containers than class and struct types. 我们来探讨一下它们之间的差异。Let's explore those differences.

命名元组和未命名元组Named and unnamed tuples

ValueTuple 结构具有名为 Item1Item2Item3 等的字段,与现有 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. 命名元组仍将元素命名为 Item1Item2Item3 等。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. IDE 和编辑器可以使用 Roslyn API 读取这些语义名称。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. 已编译的 Microsoft 中间语言 (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(双精度)。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. 例如,在以下初始值设定项中,元素为 explicitFieldOneexplicitFieldTwo,而非 localVariableOnelocalVariableTwoFor 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其值为 42stringContent(其值为“The answer to everything”):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. 示例包括 Item3ToStringExamples include Item3, ToString. Restor 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. 如果其中一个元组是可以为空值的元组,则元组相等将执行提升转换,如以下代码中所示: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. 这些转换包括提升转换、扩大转换或其他隐式转换。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");

前两个变量(unnamedanonymous)没有为元素提供语义名称。The first two variables, unnamed and anonymous do not have semantic names provided for the elements. 字段名称为 Item1Item2The field names are Item1 and Item2. 后两个变量(nameddifferentName)为元素提供了语义名称。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. Average 扩展方法一样,修正后的样本标准差公式将与平均数之差的平方的总和除以 (N-1),而不是 N。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 方法,该方法返回具有 SumSumOfSquaresCount 这三个值的元组类型: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);
}

此元组的字段被命名为 Item1Item2Item3The 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. 也可以将 objectdynamic 用作结果类型,但这种备用方法会产生高昂的性能成本。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. 可以定义一个与下面类似的类,以表示待办事项列表中的某一项: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 查询生成一个仅包含 ID 和标题的投影。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 语句将创建具有元素 IDTitle 的元组。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).

类型作者可定义一个或多个赋值给任意数量的 out 变量的 Deconstruct 方法,这类变量表示构成该类型的数据元素。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;
    }
}

该析构方法支持从 Person 赋值给两个表示 FirstNameLastName 属性的字符串: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. 以下示例显示从 Person 类型派生的 Student 类型,以及将 Student 析构成三个变量(表示 FirstNameLastNameGPA)的扩展方法: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. 如果为 student 分配三个变量,则返回名字、姓氏和 GPA。If you assign a student to three variables, the first name, last name, and GPA are all returned. 如果为 student 分配两个变量,则仅返回名字和姓氏。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. 具有相同数量的 out 参数的多个 Deconstruct 方法很快就会产生歧义。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.

在此示例中,发生有歧义的调用的几率很小,因为用于 PersonDeconstruct 方法有两个输出参数,而用于 StudentDeconstruct 方法有三个输出参数。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. 既可以获得静态类型检查的所有好处,又不需要使用更复杂的 classstruct 语法来创作类型。You get all the benefits of static type checking, without needing to author types using the more verbose class or struct syntax. 即便如此,元组还是对 privateinternal 这样的实用方法最有用。Even so, they are most useful for utility methods that are private, or internal. 当公共方法返回具有多个元素的值时,请创建用户定义的类型(classstruct 类型)。Create user-defined types, either class or struct types when your public methods return a value that has multiple elements.