c# 元组
C# 7.0 中的新增功能文章中的“元组”一节对其进行了概述。
在本文中,你将了解用于控制 C# 7.0 及更高版本中的元组的语言规则、这些规则的各种用法,以及有关如何使用元组的初步指导。
备注
System.ValueTuple
。
这些新类型添加到 .NET 标准 API 并作为框架的一部分交付后,将删除 NuGet 包要求。
借助元组,可以更轻松地对该单个对象中的多个值打包。
通过新的语言功能,可对元组中的各元素进行声明并为其赋予有意义的语义名称。
因此,元组的语言支持使用新的 ValueTuple
结构。
很多时候,你其实只是想存储单个对象中的多个值而已。
但是,这意味着在要求永久性的场合无法使用元组。
我们来探讨一下它们之间的差异。
命名元组和未命名元组
如果不为元组提供任何备用字段名称,即表示创建了一个未命名元组:
var unnamed = ("one", "two");
上例中的元组已使用文本常量进行初始化,并且不会有 C# 7.1 中使用“元组字段名称投影”创建的元素名称。
其中一种方式是在元组初始化过程中指定名称:
var named = (first: "one", second: "two");
已编译的 Microsoft 中间语言 (MSIL) 不包括为这些元素赋予的名称。
以下代码用于创建名为 accumulation
的元组,包含元素 count
(整数)和 sum
(双精度)。
var sum = 12.5;
var count = 5;
var accumulation = (count, sum);
TransformNames 列表属性,该属性包含为元组中的每个元素赋予的名称。
备注
Visual Studio 等开发工具还读取其元数据,并提供 IntelliSense 和其他使用元数据字段名称的功能。
请务必理解新元组和 ValueTuple
类型的这些基础知识,这样才能理解将命名元组赋给彼此的规则。
元组投影初始值设定项
例如,在以下初始值设定项中,元素为 explicitFieldOne
和 explicitFieldTwo
,而非 localVariableOne
和 localVariableTwo
:
var localVariableOne = 5;
var localVariableTwo = "some text";
var tuple = (explicitFieldOne: localVariableOne, explicitFieldTwo: localVariableTwo);
以下初始化表达式具有字段名称 Item1
其值为 42
和 stringContent
(其值为“The answer to everything”):
var stringContent = "The answer to everything";
var mixedTuple = (42, stringContent);
在以下两种情况下,不会将候选字段名称投影到元组字段:
- 或
Rest
。 - 候选名称重复了另一元组的显式或隐式字段名称时。
- 或
以下示例说明了这两个条件:
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 编写的代码的一项重大改变。
相等和元组
以下代码示例演示两对整数的相等比较:
var left = (a: 5, b: 10);
var right = (a: 5, b: 10);
Console.WriteLine(left == right); // displays 'true'
提升转换,如以下代码中所示:
var left = (a: 5, b: 10);
var right = (a: 5, b: 10);
(int a, int b)? nullableTuple = right;
Console.WriteLine(left == nullableTuple); // Also true
以下示例演示整数 2 元组可以与较长的 2 元组进行比较,因为进行了从整数元组到较长元组的隐式转换:
// 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
在两个操作数都为元组文本的情况下,警告位于右侧操作数,如以下示例中所述:
(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
元组相等通过嵌套元组比较每个操作数的“形状”,如以下示例中所示:
(int, (int, int)) nestedTuple = (1, (2, 3));
Console.WriteLine(nestedTuple == (1, (2, 3)) );
赋值和元组
让我们看一下元组类型之间允许的赋值类型。
注意以下示例中使用的这些变量:
// 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 = 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;
元素的赋值顺序遵循元素在元组中的顺序。
元素类型或数量不同的元组不可赋值:
// Does not compile.
// CS0029: Cannot assign Tuple(int,int,int) to Tuple(int, string)
var differentShape = (1, 2, 3);
named = differentShape;
作为方法返回值的元组
以下面的方法为例,该方法计算一个数列的标准差:
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;
}
备注
有关这些标准差公式之间的区别的更多详细信息,请查看统计信息文本。
(请记住,LINQ 查询进行迟缓计算,因此,在计算与平均数的差以及这些差的平均数时只需计算一次。)
此计算公式在计算数列时生成两个值:数列中所有项的总和,以及每个平方值的总和:
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);
}
所有这三个值都可以作为一个元组返回。
以下是更新后的版本:
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);
}
从而得到一个 private static
方法,该方法返回具有 Sum
、SumOfSquares
和 Count
这三个值的元组类型:
public static double StandardDeviation(IEnumerable<double> sequence)
{
(int Count, double Sum, double SumOfSquares) computation = ComputeSumsAnSumOfSquares(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) ComputeSumsAnSumOfSquares(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;
}
下面的代码演示了最终版本:
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);
}
这个最终版本可用于任何需要这三个值或其任意子集的方法。
该语言支持其他用于管理这些元组返回方法中的元素名称的选项。
可以删除返回值声明中的字段名称,返回一个未命名元组:
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);
}
建议为从方法返回的元组的元素提供语义名称。
最终投影的结果通常包含被选中的对象的某些(而不是全部)属性。
也可以将 object
或 dynamic
用作结果类型,但这种备用方法会产生高昂的性能成本。
可以定义一个与下面类似的类,以表示待办事项列表中的某一项:
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; }
}
返回一个元组序列的方法很好地表达了该设计:
internal IEnumerable<(int ID, string Title)> GetCurrentItemsMobileList()
{
return from item in AllItems
where !item.IsDone
orderby item.DueDate
select (item.ID, item.Title);
}
备注
在以上代码中,查询投影中的 select
语句将创建具有元素 ID
和 Title
的元组。
命名元组还承载了静态类型信息,因此无需使用高成本的运行时功能(如反射或动态绑定)来处理结果。
析构
首先,可在括号内显式声明每个字段的类型,为元组中的每个元素创建离散变量:
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
关键字,隐式声明元组中每个字段的类型化变量:
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
关键字与任意或全部变量声明结合使用。
(double sum, var sumOfSquares, var count) = ComputeSumAndSumOfSquares(sequence);
即使元组中的每个字段都具有相同的类型,也不能在括号外使用特定类型。
也可以使用现有声明析构元组:
public class Point
{
public int X { get; set; }
public int Y { get; set; }
public Point(int x, int y) => (X, Y) = (x, y);
}
警告
这将产生错误 CS8184,因为 x 在括号内声明,且 y 以前在其他位置声明。
析构用户定义类型
也可以对任何用户定义的类型(类、结构甚至接口)轻松启用析构。
例如,以下 Person
类型定义 Deconstruct
方法,该方法将 person 对象析构成表示名字和姓氏的元素:
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
赋值给两个表示 FirstName
和 LastName
属性的字符串:
var p = new Person("Althea", "Goodwin");
var (first, last) = p;
以下示例显示从 Person
类型派生的 Student
类型,以及将 Student
析构成三个变量(表示 FirstName
、LastName
和 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 分配两个变量,则仅返回名字和姓氏。
var s1 = new Student("Cary", "Totten", 4.5);
var (fName, lName, gpa) = s1;
调用方可能无法轻松调用所需的 Deconstruct
方法。
在此示例中,发生有歧义的调用的几率很小,因为用于 Person
的 Deconstruct
方法有两个输出参数,而用于 Student
的 Deconstruct
方法有三个输出参数。
下面的示例生成编译器错误 CS0019:
Person p = new Person("Althea", "Goodwin");
if (("Althea", "Goodwin") == p)
Console.WriteLine(p);
Deconstruct
方法无法将 Person
对象 p
转换为包含两个字符串的元组,但它在相等测试上下文中不适用。
结束语
即便如此,元组还是对 private
或 internal
这样的实用方法最有