System.ValueTuple 和 System.Tuple 之间的区别是什么?

我反编译了一些 C # 7库,看到使用了 ValueTuple泛型。什么是 ValueTuples,为什么不用 Tuple

59397 次浏览

什么是 ValueTuples,为什么不是 Tuple

ValueTuple是一个反映元组的结构,与原来的 System.Tuple类相同。

TupleValueTuple的主要区别是:

  • System.ValueTuple是值类型(struct) ,而 System.Tuple是引用类型(class)。在讨论分配和 GC 压力时,这是有意义的。
  • System.ValueTuple不仅仅是一个 struct,它还是一个 易变的,在使用它们的时候要小心。想想当一个类把 System.ValueTuple作为一个字段时会发生什么。
  • System.ValueTuple通过字段而不是属性公开它的项。

在 C # 7之前,使用元组并不十分方便。它们的字段名是 Item1Item2等等,而且该语言没有像其他大多数语言(Python、 Scala)那样为它们提供语法上的优势。

当。NET 语言设计团队决定合并元组并在语言层面给它们添加语法糖,一个重要的因素是性能。由于 ValueTuple是一种值类型,因此在使用它们时可以避免 GC 压力,因为(作为实现细节)它们将在堆栈上分配。

此外,struct在运行时获得自动(浅)相等语义,而 class则没有。尽管设计团队确保对元组有一个更优化的等同性,因此为它实现了一个自定义等同性。

以下是 Tuples的设计说明中的一段:

结构或类:

如前所述,我建议使元组类型 structs而不是 classes,这样就不会对它们造成分配损失 应该尽可能轻。

可以说,structs可能最终成本更高,因为任务 复制一个更大的值。因此,如果他们被分配的比他们 创建,那么 structs将是一个糟糕的选择。

但是,在它们的动机中,元组是短暂的 当部分比整体更重要的时候 模式将是构建,返回,并立即解构 在这种情况下,结构显然是可取的。

结构还有许多其他好处,它们将变成 在以下方面是显而易见的。


例子:

您可以很容易地看到,使用 System.Tuple会很快变得模棱两可。例如,假设我们有一个计算 List<Int>的和和计数的方法:

public Tuple<int, int> DoStuff(IEnumerable<int> values)
{
var sum = 0;
var count = 0;
    

foreach (var value in values) { sum += value; count++; }
   

return new Tuple(sum, count);
}

在接收端,我们最终得到:

Tuple<int, int> result = DoStuff(Enumerable.Range(0, 10));


// What is Item1 and what is Item2?
// Which one is the sum and which is the count?
Console.WriteLine(result.Item1);
Console.WriteLine(result.Item2);

将值元组解构为命名参数的方法是这个特性的真正强大之处:

public (int sum, int count) DoStuff(IEnumerable<int> values)
{
var res = (sum: 0, count: 0);
foreach (var value in values) { res.sum += value; res.count++; }
return res;
}

而在接收端:

var result = DoStuff(Enumerable.Range(0, 10));
Console.WriteLine($"Sum: {result.sum}, Count: {result.count}");

或者:

var (sum, count) = DoStuff(Enumerable.Range(0, 10));
Console.WriteLine($"Sum: {sum}, Count: {count}");

编译工具:

如果我们看看前面的例子,我们可以准确地看到编译器是如何解释 ValueTuple的,当我们要求它解构:

[return: TupleElementNames(new string[] {
"sum",
"count"
})]
public ValueTuple<int, int> DoStuff(IEnumerable<int> values)
{
ValueTuple<int, int> result;
result..ctor(0, 0);
foreach (int current in values)
{
result.Item1 += current;
result.Item2++;
}
return result;
}


public void Foo()
{
ValueTuple<int, int> expr_0E = this.DoStuff(Enumerable.Range(0, 10));
int item = expr_0E.Item1;
int arg_1A_0 = expr_0E.Item2;
}

在内部,编译后的代码使用 Item1Item2,但是所有这些都是从我们抽象出来的,因为我们使用的是一个分解的元组。带有命名参数的元组用 TupleElementNamesAttribute进行注释。如果我们使用一个新变量而不是分解,我们得到:

public void Foo()
{
ValueTuple<int, int> valueTuple = this.DoStuff(Enumerable.Range(0, 10));
Console.WriteLine(string.Format("Sum: {0}, Count: {1})", valueTuple.Item1, valueTuple.Item2));
}

请注意,当我们调试应用程序时,编译器仍然需要使一些奇迹发生(通过属性) ,因为看到 Item1Item2是很奇怪的。

我查看了 TupleValueTuple的源代码。区别在于,Tupleclass,而 ValueTuple是实现 IEquatablestruct

这意味着如果它们不是同一个实例,Tuple == Tuple将返回 false,但如果它们属于同一类型,ValueTuple == ValueTuple将返回 true,而 Equals将为它们包含的每个值返回 true

TupleValueTuple的区别在于,Tuple是引用类型,而 ValueTuple是值类型。后者是可取的,因为对 C # 7语言的更改使得元组的使用频率大大提高,但是为每个元组在堆上分配一个新对象是一个性能问题,特别是在没有必要的情况下。

但是,在 C # 7中,其思想是从不显式地使用 中的任何一种类型,因为添加了用于 tuple 的语法 Sugar。例如,在 C # 6中,如果你想使用元组来返回一个值,你必须执行以下操作:

public Tuple<string, int> GetValues()
{
// ...
return new Tuple(stringVal, intVal);
}


var value = GetValues();
string s = value.Item1;

然而,在 C # 7中,您可以使用以下代码:

public (string, int) GetValues()
{
// ...
return (stringVal, intVal);
}


var value = GetValues();
string s = value.Item1;

您甚至可以更进一步,为这些值命名:

public (string S, int I) GetValues()
{
// ...
return (stringVal, intVal);
}


var value = GetValues();
string s = value.S;

或者完全解构元组:

public (string S, int I) GetValues()
{
// ...
return (stringVal, intVal);
}


var (S, I) = GetValues();
string s = S;

元组在 C # pre-7中并不经常使用,因为它们非常麻烦和冗长,而且只有在只为一个工作实例构建数据类/结构的时候才会真正使用它。但是在 C # 7中,元组现在有了语言级别的支持,所以使用它们更加简洁和有用。

其他的答案忘了提到重点,我将引用来自 源代码的 XML 文档,而不是重新措辞:

ValueTuple 类型(从0到8)包含基础的运行时实现 C # 中的元组和 F # 中的结构元组。

除了通过语言语法 创建外,最容易通过 ValueTuple.Create工厂方法。 System.ValueTuple型与 System.Tuple型的不同之处在于:

  • 它们是结构而不是类,
  • 它们是可变的,而不是只读的
  • 它们的成员(如 Item1、 Item2等)是字段而不是属性。

通过引入这种类型和 C # 7.0编译器,您可以轻松地编写

(int, string) idAndName = (1, "John");

并从方法返回两个值:

private (int, string) GetIdAndName()
{
//.....
return (id, name);
}

System.Tuple相反,你可以更新它的成员(Mutable) ,因为它们是公共读写字段,可以给予有意义的名称:

(int id, string name) idAndName = (1, "John");
idAndName.name = "New Name";

除了上面的注释之外,ValueTuple 的一个不幸的问题是,作为一种值类型,命名参数在编译为 IL 时会被擦除,因此它们在运行时不可用于序列化。

也就是说,当通过 Json.NET 进行序列化时,你的名字甜美的参数仍然会以“ Item1”、“ Item2”等结尾。

迟加入对这两个事实加以快速澄清:

  • 它们是结构而不是类
  • 它们是可变的,而不是只读的

人们可能会认为,大规模地改变值元组很简单:

 foreach (var x in listOfValueTuples) { x.Foo = 103; } // wont even compile because x is a value (struct) not a variable


var d = listOfValueTuples[0].Foo;

有人可能会试图这样解决这个问题:

 // initially *.Foo = 10 for all items
listOfValueTuples.Select(x => x.Foo = 103);


var d = listOfValueTuples[0].Foo; // 'd' should be 103 right? wrong! it is '10'

这种古怪行为的原因是值元组正是基于值的(结构) ,因此。选择(...)调用对克隆的结构体有效,而不是对原始结构体有效。为了解决这个问题,我们必须采取以下措施:

 // initially *.Foo = 10 for all items
listOfValueTuples = listOfValueTuples
.Select(x => {
x.Foo = 103;
return x;
})
.ToList();


var d = listOfValueTuples[0].Foo; // 'd' is now 103 indeed

当然,人们也可以尝试直截了当的方法:

   for (var i = 0; i < listOfValueTuples.Length; i++) {
listOfValueTuples[i].Foo = 103; //this works just fine


// another alternative approach:
//
// var x = listOfValueTuples[i];
// x.Foo = 103;
// listOfValueTuples[i] = x; //<-- vital for this alternative approach to work   if you omit this changes wont be saved to the original list
}


var d = listOfValueTuples[0].Foo; // 'd' is now 103 indeed

希望这可以帮助那些正在努力从列表托管的值元组中弄清楚正反面的人。