元组
实际开发中我们经常会遇到需要把几个相关的数据临时组合在一起的场景,比如一个方法需要返回多个结果。传统做法是使用out参数(见数据类型章节),或者专门定义一个类或结构体来承载这些数据,但这些写法要么别扭,要么为了一次性用途专门定义类型很不划算。C# 7.0引入了元组(Tuple)语法,它可以把多个不同类型的数据直接组合成一个轻量级的值来使用,是处理这类场景的现代方案。
C#中的元组基于System.ValueTuple结构体实现,是值类型。注意.NET早期还有一个基于类的System.Tuple,它是引用类型且用法繁琐,属于历史遗留,新代码中不应再使用,本文介绍的均为ValueTuple。
元组的声明和初始化
声明元组时,使用小括号将各元素的类型括起来,元素之间使用逗号分隔。
// 未命名元素
(int, string) t1 = (1, "hello");
// 命名元素,推荐写法
(int Id, string Name) t2 = (1, "Tom");
// 声明和初始化也可以分开
(int Id, string Name) t3;
t3 = (2, "Jerry");
元组也支持使用var进行类型推导。
var t = (1, "hello");
访问元组元素
声明时如果没有为元素命名,各元素使用Item1、Item2这样的默认名称访问。
var t = (1, "hello");
Console.WriteLine(t.Item1); // 1
Console.WriteLine(t.Item2); // hello
如果为元素命了名,则可以直接使用名称访问,可读性更好,实际开发中推荐尽量为元素命名。
var student = (Id: 1, Name: "Tom");
Console.WriteLine(student.Id); // 1
Console.WriteLine(student.Name); // Tom
C# 7.1还引入了元素名推断,当初始化表达式是变量或属性访问时,编译器会自动推断出元素名。
int id = 1;
string name = "Tom";
// 元素名自动推断为 Id 和 Name
var student = (id, name);
Console.WriteLine(student.name); // Tom
元组的解构
元组支持解构(Deconstruction)语法,可以一步把元组的各个元素拆分到独立的变量中。
var student = (Id: 1, Name: "Tom");
// 解构为两个独立变量
var (id, name) = student;
Console.WriteLine(id); // 1
Console.WriteLine(name); // Tom
解构时如果不需要某个元素,可以使用弃元_将其丢弃。
var (_, name) = student; // 只取Name,丢弃Id
使用元组返回多个值
元组最典型的应用场景是让方法返回多个值,替代别扭的out参数写法。
class Program
{
// 同时返回商和余数
static (int Quotient, int Remainder) Divide(int a, int b)
{
return (a / b, a % b);
}
static void Main()
{
var (quotient, remainder) = Divide(7, 3);
Console.WriteLine("商 {0} 余数 {1}", quotient, remainder); // 商 2 余数 1
}
}
和out参数相比,元组写法返回值就是普通的方法返回值,可以参与链式调用,也可以在异步方法中使用(异步方法的参数不允许使用out和ref修饰,这也是out方案在异步代码中行不通的原因)。
元组的相等性
C# 7.3起,元组支持==和!=运算符,按各元素的值逐一比较。
var t1 = (1, "hello");
var t2 = (1, "hello");
Console.WriteLine(t1 == t2); // True,按值比较
由于元组是值类型且支持按值比较,元组也可以直接作为Dictionary的键使用。
元组与record、匿名类型的对比
C#中能够「临时组合多个数据」的方案不止元组一种,这里对三者的适用场景做一个对比。
| 方案 | 类型 | 元素命名 | 主要适用场景 |
|---|---|---|---|
| 元组 | 值类型(ValueTuple) |
可命名,仅存在于编译期 | 方法返回多个值、临时数据组合 |
| 匿名类型 | 引用类型,编译器自动生成 | 必须命名 | LINQ查询投影(见LINQ章节) |
record |
引用类型(record struct为值类型) |
必须命名 | 需要复用、对外暴露的数据模型(见类和接口章节) |
简单来说:一次性、局部性的数据组合用元组最省事;LINQ投影用匿名类型;数据需要跨方法传递、对外暴露或复用时,则应当定义为record或普通的类。元组还有一个限制需要注意,它的元素名只是编译期的语法标注,编译后并不保留,因此通过反射等机制无法获取元素名,跨程序集作为公开API的返回值时应优先考虑record。