首页 > 编程笔记 > C#笔记 阅读:1

C# tuple元组用法详解(附带实例)

C# 中的元组(tuple)和匿名类型一样,也是存储一组值的便捷方式。元组的主要目的是不使用 out 参数而从方法中返回多个值(这是匿名类型做不到的)。

元组几乎可以做到匿名类型做到的任何事情,甚至更多。而它的缺点之一是运行命名元素时会擦除类型,我们接下来将会进行介绍。

创建元组字面量的最简单方式是在括号中列出期望的值。这样就可以创建一个包含匿名元素的元组,并使用 Item1、Item2 等访问其中的元素:
var bob = ("Bob", 23);          // Allow compiler to infer the element types
Console.WriteLine(bob.Item1);   // Bob
Console.WriteLine(bob.Item2);   // 23

元组是值类型,并且其中的元素是可变(可读可写)的:
var joe = bob;          // joe is a *copy* of bob
joe.Item1 = "Joe";      // Change joe's Item1 from Bob to Joe
Console.WriteLine(bob); // (Bob, 23)
Console.WriteLine(joe); // (Joe, 23)

和匿名类型不同,我们可以将每一个元素的类型列在括号中来显式指定元组的类型:
(string, int) bob = ("Bob", 23);
这意味着我们可以有效地从方法中返回元组:
(string, int) person = GetPerson();   // Could use 'var' instead if we want
Console.WriteLine(person.Item1);      // Bob
Console.WriteLine(person.Item2);      // 23

(string, int) GetPerson() => ("Bob", 23);

元组和泛型配合默契,因此以下类型都是合法的:
Task<(string, int)>
Dictionary<(string, int), Uri>
IEnumerable<(int id, string name)>     // See below for naming elements

C#元组元素命名

创建元组字面量时,可以为其中的元素起一些有意义的名字:
var tuple = (name: "Bob", age: 23);
Console.WriteLine(tuple.name);   // Bob
Console.WriteLine(tuple.age);    // 23

当然也可以在指定元组类型时进行命名:
var person = GetPerson();
Console.WriteLine(person.name);   // Bob
Console.WriteLine(person.age);    // 23

(string name, int age) GetPerson() => ("Bob", 23);
需要指出的是,即使进行了命名也可以像匿名时那样使用 Item1、Item2 等来引用元素(虽然 Visual Studio 会在 IntelliSense 中隐藏这些字段)。

元素命名可以由属性或字段命名直接推断得出:
var now = DateTime.Now;
var tuple = (now.Day, now.Month, now.Year);
Console.WriteLine(tuple.Day);   // OK

如果元组(按顺序)对应的元素类型相同,则元组是类型兼容的,而其中的元素命名可以不同:
(string name, int age, char sex) bob1 = ("Bob", 23, 'M');
(string age, int sex, char name) bob2 = bob1;   // No error!

上述例子也会导致令人困惑的结果:
Console.WriteLine(bob2.name);   // M
Console.WriteLine(bob2.age);    // Bob
Console.WriteLine(bob2.sex);    // 23

我们之前提到过,C# 编译器会为匿名类型创建自定义类并为每一个元素创建命名的属性。而在处理元组时则借助了一系列现存的泛型结构体,这和匿名对象的处理方式是非常不同的:
public struct ValueTuple<T1>
public struct ValueTuple<T1, T2>
public struct ValueTuple<T1, T2, T3>
每一个 ValueTuple<> 结构体都有 Item1、Item2 等字段。

因此,(string, int) 是 ValueTuple<string,int> 的别名。同时,这意味着命名的元组元素并没有底层类型的命名属性的支撑。这些名字仅仅存在于源代码和编译器的“想象”中。在运行时,这些名字大多会消失。当我们反编译引用命名元素的元组时,可以看到程序仅仅引用了 Item1、Item2 等这样的字段。

若将元组变量赋值给一个 object 对象并在调试器下观察(或者在 LINQPad 下输出),就可以发现元素的名字完全消失了。因此,在绝大多数情况下,都不能用反射的方式确定元组在运行时的命名。

刚才提到元组的命名在大部分情况下都消失了,那么就有例外的情况。当方法或属性返回命名元组类型时,编译器会将一个自定义特性 TupleElem-entNamesAttribute 附加到成员的返回类型上以生成元素名称。这样命名元素就可以支持跨程序集的方法调用了(注意,在这种情况下,编译器没有可供参考的源代码)。

除前面提到的方法外,还可以在非泛型的 ValueTuple 类型上调用工厂方法来创建元组:
ValueTuple<string, int> bob1 = ValueTuple.Create("Bob", 23);
(string, int) bob2 = ValueTuple.Create("Bob", 23);
(string name, int age) bob3 = ValueTuple.Create("Bob", 23);

C#元组的解构

元组隐式支持解构模式,因此可以将一个元组解构为独立的变量。考虑以下代码:
var bob = ("Bob", 23);
string name = bob.Item1;
int age = bob.Item2;

使用元组的解构器可以简写为:
var bob = ("Bob", 23);
(string name, int age) = bob;   // Deconstruct the bob tuple into
                                // separate variables (name and age)
Console.WriteLine(name);
Console.WriteLine(age);

解构元组的语法和声明一个含有命名元素的元组的语法有很多相似之处。下面的例子指出了它们的区别:
(string name, int age) = bob;   // Deconstructing a tuple
(string name, int age) bob2 = bob;   // Declaring a new tuple

以下是另一个例子,其中包含了方法调用和类型推断(var):
var (name, age, sex) = GetBob();
Console.WriteLine(name);   // Bob
Console.WriteLine(age);    // 23
Console.WriteLine(sex);    // M

(string, int, char) GetBob() => ("Bob", 23, 'M');

除此之外,还可以直接将元组解构到字段和属性,简化构造器中的多字段或属性的赋值:
class Point
{
    public readonly int X, Y;
    public Point(int x, int y) => (X, Y) = (x, y);
}

C#元组的比较

和匿名类型一样,元组的 Equals 方法也执行结构化相等比较。这意味着它比较的也是内部的数据而不是引用:
var t1 = ("one", 1);
var t2 = ("one", 1);
Console.WriteLine(t1.Equals(t2));   // True

ValueTuple<> 类型还重载了 == 和 != 运算符:
Console.WriteLine(t1 == t2);   // True (from C# 7.3)
当然,它也重写了 GetHashCode 方法,因此元组对象可以用作字典中的键。

ValueTuple<> 类型实现了 IComparable 接口,因此元组也可以作为排序的依据。

C# System.Tuple类

在 System 命名空间下还存在着另一类泛型类型:Tuple(而不是 ValueTuple),是在 2010 年引入的。

Tuple 是类,而 ValueTuple 类型是结构体。反思之后,人们发现将元组定义为类这一决定是错误的,在典型的元组使用场景中,结构体有一些性能优势(避免了不必要的内存分配)且几乎没有任何缺点。因此微软在 C# 7 中增加了对元组的语言级支持,推荐使用新的 ValueTuple 而忽略之前的 Tuple 类型。

我们还能在 C# 7 之前的代码中发现 Tuple 类的影子,但它们没有任何语言上的特殊支持,例如:
Tuple<string, int> t = Tuple.Create("Bob", 23);   // Factory method
Console.WriteLine(t.Item1);   // Bob
Console.WriteLine(t.Item2);   // 23

相关文章