和匿名类型一样,元组(tuple)也是存储一组值的便捷方式。元组的主要目的是不使用 out 参数而从方法中返回多个值(这是匿名类型做不到的)。
元组几乎可以做到匿名类型做到的任何事情,甚至更多。而它的缺点之一是运行命名元素时会擦除类型,我们接下来将会进行介绍。
创建元组字面量的最简单方式是在括号中列出期望的值。这样就可以创建一个包含匿名元素的元组,并使用 Item1 、 Item2 等访问其中的元素:
元组是值类型,并且其中的元素是可变(可读可写)的:
和匿名类型不同,我们可以将每一个元素的类型列在括号中来显式指定元组的类型:
这意味着我们可以有效地从方法中返回元组:
元组和泛型配合默契,因此以下类型都是合法的:
创建元组字面量时,可以为其中的元素起一些有意义的名字:
当然也可以在指定元组类型时进行命名:
需要指出的是,即使进行了命名也可以像匿名时那样使用 Item1 、 Item2 等来引用元素(虽然Visual Studio会在IntelliSense中隐藏这些字段)。
元素命名可以由属性或字段命名直接推断得出:
如果元组(按顺序)对应的元素类型相同,则元组是类型兼容的,而其中的元素命名可以不同:
上述例子也会导致令人困惑的结果:
我们之前提到过C#编译器会为匿名类型创建自定义类并为每一个元素创建命名的属性。而在处理元组时则借助了一系列现存的泛型结构体,这和匿名对象的处理方式是非常不同的:
每一个 ValueTuple<> 结构体都有 Item1 、 Item2 等字段。
因此, (string,int) 是 ValueTuple<string,int> 的别名。同时,这意味着命名的元组元素并没有底层类型的命名属性的支撑。这些名字仅仅存在于源代码和编译器的“想象”中。在运行时,这些名字大多会消失。当我们反编译引用命名元素的元组时,可以看到程序仅仅引用了 Item1 、 Item2 等这样的字段。若将元组变量赋值给一个 object 对象并在调试器下观察(或者在LINQPad下输出),就可以发现元素的名字完全消失了。因此,在绝大多数情况下,都不能用反射(请参见第18章)的方式确定元组在运行时的命名。
刚才提到元组的命名在大部分情况下都消失了,那么就有例外的情况。当方法或属性返回命名元组类型时,编译器会将一个自定义特性 TupleElem-entNamesAttribute 附加到成员的返回类型上以生成元素名称(请参见4.14节)。这样命名元素就可以支持跨程序集的方法调用了(注意,在这种情况下,编译器没有可供参考的源代码)。
除前面提到的方法外,还可以在非泛型的 ValueTuple 类型上调用工厂方法来创建元组:
元组隐式支持解构模式(请参见3.1.5节),因此可以将一个元组解构为独立的变量。考虑以下代码:
使用元组的解构器可以简写为:
解构元组的语法和声明一个含有命名元素的元组的语法有很多相似之处。下面的例子指出了它们的区别:
以下是另一个例子,其中包含了方法调用和类型推断( var ):
除此之外,还可以直接将元组解构到字段和属性,简化构造器中的多字段或属性的赋值:
和匿名类型一样,元组的 Equals 方法也执行结构化相等比较。这意味着它比较的也是内部的数据而不是引用:
ValueTuple<> 类型还重载了 == 和 != 运算符:
当然,它也重写了 GetHashCode 方法,因此元组对象可以用作字典中的键。关于相等比较的话题,请参见6.13节以及7.5节。
ValueTuple<> 类型实现了 IComparable 接口(请参见6.14节),因此元组也可以作为排序的依据。
在 System 命名空间下还存在着另一类泛型类型: Tuple (而不是 ValueTuple )。 Tuple 是在2010年引入的。 Tuple 是类,而 ValueTuple 类型是结构体。反思之后,人们发现将元组定义为类这一决定是错误的,在典型的元组使用场景中,结构体有一些性能优势(避免了不必要的内存分配)且几乎没有任何缺点。因此微软在C# 7中增加了对元组的语言级支持,推荐使用新的 ValueTuple 而忽略之前的 Tuple 类型。我们还能在C# 7之前的代码中发现 Tuple 类的影子,但它们没有任何语言上的特殊支持,例如: