对元组的语言支持

基本

一个元组是元素的有序,有限的名单。元组通常在编程中用作与单个实体一起工作的手段,而不是单独使用每个元组的元素,并在关系数据库中表示各个行(即记录)。

在 C#7.0 中,方法可以有多个返回值。在幕后,编译器将使用新的 ValueTuple 结构。

public (int sum, int count) GetTallies() 
{
    return (1, 2);
}

附注 :为了在 Visual Studio 2017 中工作,你需要获取 System.ValueTuple 包。

如果将元组返回方法结果分配给单个变量,则可以通过方法签名上的已定义名称访问成员:

var result = GetTallies();
// > result.sum
// 1
// > result.count
// 2

元组解构

元组解构将元组分成了它的部分。

例如,调用 GetTallies 并将返回值赋值给两个单独的变量会将元组解构为这两个变量:

(int tallyOne, int tallyTwo) = GetTallies();

var 也有效:

(var s, var c) = GetTallies();

你也可以使用更短的语法,var 以外的 var

var (s, c) = GetTallies();

你还可以解构为现有变量:

int s, c;
(s, c) = GetTallies();

交换现在变得更加简单(不需要临时变量):

(b, a) = (a, b);

有趣的是,任何对象都可以通过在类中定义 Deconstruct 方法来解构:

class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public void Deconstruct(out string firstName, out string lastName)
    {
        firstName = FirstName;
        lastName = LastName;
    }
}

var person = new Person { FirstName = "John", LastName = "Smith" };
var (localFirstName, localLastName) = person;

在这种情况下,(localFirstName, localLastName) = person 语法在 person 上调用 Deconstruct

甚至可以用扩展方法来定义解构。这相当于上面的内容:

public static class PersonExtensions
{
    public static void Deconstruct(this Person person, out string firstName, out string lastName)
    {
        firstName = person.FirstName;
        lastName = person.LastName;
    }
}

var (localFirstName, localLastName) = person;

Person 类的另一种方法是将 Name 本身定义为 Tuple。考虑以下:

class Person
{
    public (string First, string Last) Name { get; }

    public Person((string FirstName, string LastName) name)
    {
        Name = name;
    }
}

然后你可以像这样实例化一个人(我们可以把一个元组作为参数):

var person = new Person(("Jane", "Smith"));

var firstName = person.Name.First; // "Jane"
var lastName = person.Name.Last;   // "Smith"

元组初始化

你还可以在代码中任意创建元组:

var name = ("John", "Smith");
Console.WriteLine(name.Item1);
// Outputs John

Console.WriteLine(name.Item2);
// Outputs Smith

创建元组时,你可以为元组的成员分配特殊项目名称:

var name = (first: "John", middle: "Q", last: "Smith");
Console.WriteLine(name.first);
// Outputs John

类型推断

使用相同签名(匹配类型和计数)定义的多个元组将被推断为匹配类型。例如:

public (int sum, double average) Measure(List<int> items)
{
    var stats = (sum: 0, average: 0d);
    stats.sum = items.Sum();
    stats.average = items.Average();
    return stats;
}

stats 可以返回,因为 stats 变量的声明和方法的返回签名是匹配的。

反射和元组字段名称

成员名称在运行时不存在。即使成员名称不匹配,Reflection 也会考虑具有相同数量和类型的成员的元组。将元组转换为 object 然后转换为具有相同成员类型但名称不同的元组也不会导致异常。

虽然 ValueTuple 类本身不保留成员名称的信息,但可以通过 TupleElementNamesAttribute 中的反射获得信息。此属性不应用于元组本身,而是应用于方法参数,返回值,属性和字段。这使得元组项目名称被保留整个组件,即如果一个方法返回(字符串名称,诠释计数)名称名称和数量将可在另一组装方法的调用者,因为返回值将包含值 TupleElementNameAttribute 标记名字计数

与泛型和 async 一起使用

新的元组功能(使用底层的 ValueTuple 类型)完全支持泛型,可以用作泛型类型参数。这使得它可以与 async / await 模式一起使用:

public async Task<(string value, int count)> GetValueAsync()
{
    string fooBar = await _stackoverflow.GetStringAsync();
    int num = await _stackoverflow.GetIntAsync();

    return (fooBar, num);
}

与集合一起使用

在一个场景中有一个元组集合可能会变得有益,在这个场景中,你试图根据条件找到匹配的元组以避免代码分支。

例:

private readonly List<Tuple<string, string, string>> labels = new List<Tuple<string, string, string>>()
{
    new Tuple<string, string, string>("test1", "test2", "Value"),
    new Tuple<string, string, string>("test1", "test1", "Value2"),
    new Tuple<string, string, string>("test2", "test2", "Value3"),
};

public string FindMatchingValue(string firstElement, string secondElement)
{
    var result = labels
        .Where(w => w.Item1 == firstElement && w.Item2 == secondElement)
        .FirstOrDefault();

    if (result == null)
        throw new ArgumentException("combo not found");

    return result.Item3;
}

随着新元组可以成为:

private readonly List<(string firstThingy, string secondThingyLabel, string foundValue)> labels = new List<(string firstThingy, string secondThingyLabel, string foundValue)>()
{
    ("test1", "test2", "Value"),
    ("test1", "test1", "Value2"),
    ("test2", "test2", "Value3"),
}

public string FindMatchingValue(string firstElement, string secondElement)
{
    var result = labels
        .Where(w => w.firstThingy == firstElement && w.secondThingyLabel == secondElement)
        .FirstOrDefault();

    if (result == null)
        throw new ArgumentException("combo not found");

    return result.foundValue;
}

虽然上面示例元组的命名非常通用,但相关标签的概念允许更深入地理解代码中引用 item1item2item3 的内容。

ValueTuple 和 Tuple 之间的差异

引入 ValueTuple 的主要原因是性能。

输入名称 ValueTuple Tuple
类或结构 struct class
可变性(创建后改变值) 易变的 一成不变
命名成员和其他语言支持 不( TBD

参考