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);
这些情况不会导致编译器错误,因为当元组字段名称投影不可用时,它将成为使用 C# 7.0 编写的代码的一项重大改变。
相等和元组
以下代码示例演示两对整数的相等比较:
var left = (a: 5, b: 10);
var right = (a: 5, b: 10);
Console.WriteLine(left == right);
提升转换,如以下代码中所示:
var left = (a: 5, b: 10);
var right = (a: 5, b: 10);
(int a, int b)? nullableTuple = right;
Console.WriteLine(left == nullableTuple);
以下示例演示整数 2 元组可以与较长的 2 元组进行比较,因为进行了从整数元组到较长元组的隐式转换:
在两个操作数都为元组文本的情况下,警告位于右侧操作数,如以下示例中所述:
(int a, string b) pair = (1, "Hello");
(int z, string y) another = (1, "Hello");
Console.WriteLine(pair == another);
元组相等通过嵌套元组比较每个操作数的“形状”,如以下示例中所示:
(int, (int, int)) nestedTuple = (1, (2, 3));
Console.WriteLine(nestedTuple == (1, (2, 3)) );
赋值和元组
让我们看一下元组类型之间允许的赋值类型。
注意以下示例中使用的这些变量:
这两个元组具有不同的元素名称。
因此可进行以下赋值:
unnamed = named;
named = unnamed;
元素的赋值顺序遵循元素在元组中的顺序。
元素类型或数量不同的元组不可赋值:
作为方法返回值的元组
以下面的方法为例,该方法计算一个数列的标准差:
public static double StandardDeviation(IEnumerable<double> sequence)
{
备注
有关这些标准差公式之间的区别的更多详细信息,请查看统计信息文本。
(请记住,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 这样的实用方法最有
|
请发表评论