10分钟内理解C#中的元组入门
[[academy-video-youtube({"vid": "QC6hpl2iU0c", "start_time": "0", "title": "C# 中的 Tuples 简介 10 分钟内完成", "creator": "Tim Corey", "length": "9m 49s"})]]
C#方法是围绕单个返回值设计的,这在一个返回值真正不足的情况下一直有效。 地址验证是经典示例:您需要知道输入是否有效,如果是,您希望返回经过清理的标准形式。 将该对包装在一个新的类中对于一个方法来说显得笨重,并且输出参数以一种过时的方式混合了输入和输出。 元组被添加到语言中,因此这种确切情况有了更简洁的答案。
在他的视频"Introduction to Tuples in C# In 10 Minutes or Less"中,Tim Corey通过构建一个小型地址验证方法,展示了需要同时返回格式化字符串和有效性标志的元组。 在此过程中,他介绍了返回元组的语法、为什么命名成员很重要、如何在调用站点将结果拆分为独立变量以及如何使用弃用字符跳过您不在乎的部分。 习惯于使用一次性结果类或out参数的读者会发现这里有一个更轻便的模式。
问题所在:返回多个值
[0:18 - 2:50] Tim以一个接受街道地址字符串的ValidateAddress方法开场。 他想要的行为很简单:如果输入匹配"123 Sesame St",返回正确格式化的"123 Sesame Street"; 否则,表明地址无效。 从一个调用中需要返回两条信息,清理后的字符串和成功标志。
元组之前的选项都显得笨拙。 创建一个ValidateAddressResult类是可以的,但它在项目中留下了一个仅用于特定目的的类型,其他代码将永远不会实例化它。 使用输出参数通过参数列表发送格式化地址也可以奏效,但它在方法签名中混合了输入和输出,使得调用站点更难阅读。 元组被引入以处理这个问题,而不需要支付任一代价。
返回未命名的元组
[2:50 - 4:01] 元组返回类型的最小语法将两个类型包裹在括号中:
public (string, bool) ValidateAddress(string address)
{
if (address == "123 Sesame St")
{
return ("123 Sesame Street", true);
}
return (address, false);
}public (string, bool) ValidateAddress(string address)
{
if (address == "123 Sesame St")
{
return ("123 Sesame Street", true);
}
return (address, false);
}该方法现在返回一个(string, bool)对。 当尝试使用它时,缺点立刻显现。 由于未给成员分配名称,IntelliSense将它们显示为Item2,这让下一个读者无法了解每个值代表什么。 Tim将此标记为他从不发布未命名元组的原因,即使语法最短。六个月后阅读result.Item2的成本比输入一次名称的成本更高。
命名元组成员
[4:01 - 6:30] 为元组的每个成员赋名是在返回类型声明中的一个单行更改:
public (string Address, bool IsValid) ValidateAddress(string address)
{
if (address == "123 Sesame St")
{
return ("123 Sesame Street", true);
}
return (address, false);
}public (string Address, bool IsValid) ValidateAddress(string address)
{
if (address == "123 Sesame St")
{
return ("123 Sesame Street", true);
}
return (address, false);
}现在调用站点可以自然地读取:
var result = ValidateAddress("123 Sesame St");
if (result.IsValid)
{
Console.WriteLine($"Your validated address is {result.Address}");
}
else
{
Console.WriteLine("That is an invalid address");
}var result = ValidateAddress("123 Sesame St");
if (result.IsValid)
{
Console.WriteLine($"Your validated address is {result.Address}");
}
else
{
Console.WriteLine("That is an invalid address");
}用"123 Sesame St"运行代码,打印出规范形式。 去掉开头的1以生成"23 Sesame St"使IsValid返回false,并打印无效地址消息。 方法的契约现在在调用站点自文档化,而不需要命名元组要求支持类文件。
一个元组可以承载超过两个成员。 三个、四个或五个以相同的方式工作。 Tim指出,一旦元组超过三四个成员,记录或类通常成为更清晰的选择,但对于一个方法中两个或三个相关值而言,元组仍是更轻量的选项。
解构元组为独立变量
[6:30 - 7:39] 有时调用者宁愿将结果的每一部分保存到自己的变量中,而不是将它们集中在一个result中。 C#直接支持解构语法:
(string message, bool valid) = ValidateAddress("123 Sesame St");
if (valid)
{
Console.WriteLine($"Your validated address is {message}");
}(string message, bool valid) = ValidateAddress("123 Sesame St");
if (valid)
{
Console.WriteLine($"Your validated address is {message}");
}左侧的变量名无需与方法上的元组成员名匹配。 这里IsValid。 Tim指出,这是一种解构器,而不是一个解构器; 这两个词听起来相似,但指的是完全不同的概念。解构器将元组拆分为调用者真正需要的局部变量。
使用弃置字符跳过值
[7:41 - 8:54] 当调用者只关心返回值的子集时,丢弃字符(_)会移除他们不需要的值:
(string message, _) = ValidateAddress("123 Sesame St");
Console.WriteLine(message);(string message, _) = ValidateAddress("123 Sesame St");
Console.WriteLine(message);下划线告诉编译器"我故意忽略这个值"。这比将不需要的成员分配给一个随意的变量名如unused更具表现力,因为一个弃用表明了意图。 任何阅读这行代码的人都知道忽略IsValid是这个调用位置的一个故意选择,而不是疏忽。
总结:当元组配得上其位置
[8:54 - 9:28] 元组的主要用途是从一个方法返回多个值,而不创建只为一次返回而存在的类。 在方法签名中命名成员,在调用站点解构它们,并丢掉调用者不需要的部分,是让元组得以生存的三个模式。 除此之外,元组在LINQ投影和阶段性计算中也很有用,在这里一个快速配对的值比匿名类型更易读。
结论
[9:28 - 9:49] 元组给C#提供了一种紧凑的方法,用于从一个方法返回多个值。 具名形式是日常使用的版本,因为result.Item1不可读。 解构在调用者更喜欢这种形状时,将部分提升为局部变量,并且弃置字符使得一个调用只取所需的东西。
示例提示:当您开始在三个或更多地方解构相同的元组形状时,这就是要升级为记录的信号。 当形状局限于一个方法时,元组会闪闪发光; 一旦形状成为域的一部分,命名类型所支付的代价会得以偿还。

