我反编译了一些C# 7的库,看到使用了ValueTuple
泛型。那么什么是ValueTuples
,为什么不用Tuple
呢?
我反编译了一些C# 7的库,看到使用了ValueTuple
泛型。那么什么是ValueTuples
,为什么不用Tuple
呢?
什么是ValueTuples,为什么不用Tuple呢?
一个ValueTuple是一个反映元组的结构体,与原始的System.Tuple类相同。
Tuple和ValueTuple的主要区别是:
System.ValueTuple
是一个值类型(结构体),而 System.Tuple
是一个引用类型(类)。在谈论分配和垃圾回收压力时,这是有意义的。System.ValueTuple
不仅仅是一个 struct
,它是一个可变的,因此在使用它们时必须小心。想象一下当一个类将 System.ValueTuple
作为字段时会发生什么。System.ValueTuple
通过字段而不是属性公开其项。直到 C# 7,使用元组并不是很方便。它们的字段名为 Item1
、Item2
等,并且语言没有像大多数其他语言(如 Python、Scala)那样为它们提供语法糖。
当 .NET 语言设计团队决定在语言级别上合并元组并为它们添加语法糖时,性能是一个重要因素。由于 ValueTuple
是一个值类型,使用它们时可以避免垃圾回收压力,因为(作为实现细节)它们将在堆栈上分配。
如前所述,我建议将元组类型定义为结构体
而不是类
,以避免与它们相关的分配开销。它们应该尽可能轻量级。
可以说,结构体
可能会更加昂贵,因为赋值会复制一个较大的值。因此,如果它们被赋值的次数远远超过创建的次数,那么选择结构体
就是一个糟糕的选择。
然而,在它们的动机上,元组是短暂的。当部分比整体更重要时,您会使用它们。因此,常见的模式是构造、返回和立即解构它们。在这种情况下,结构体显然更可取。
结构体还有许多其他好处,接下来将变得明显。
很容易看出,使用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;
}
Item1
和Item2
,但由于我们使用的是分解的元组,所有这些都被抽象化了。带有命名参数的元组会被注释为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));
}
Item1
,Item2
是很奇怪的。var (sum, count) = DoStuff(Enumerable.Range(0, 10));
。 - Abion47Tuple
和ValueTuple
的区别在于Tuple
是引用类型,而ValueTuple
是值类型。后者更可取,因为在C# 7中对元组的语言支持更多,但每个元组都分配一个新的对象到堆上将产生性能问题,特别是在不必要时。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# 7 之前,元组很少使用,因为它们很笨重冗长,只有在构建数据类/结构体仅用于单个工作实例的情况下才会被使用,这往往不划算。但在 C# 7 中,元组现在具有语言级别的支持,因此使用它们更加清晰和有用。
(string S, int I)
,那么它就不再是Tuple
类型了,而是ValueTuple
类型,这是两种不同的东西。如果你返回一个类的私有字段,那么从性能角度考虑,使用引用类型的Tuple
可能更好? - joeValueTuple
提供的语言支持和有用工具的支持,换取了返回时间上的几个纳秒。另一方面,每当需要取消引用(即解箱)值时,您也会失去至少那么多时间。最后,如果您将元组存储为私有类值,则我认为这是代码异味。如果您将数据包作为类数据存储,请使用类/结构,而不是元组。 - Abion47int
,bool
甚至在某种程度上是string
这样的原始值类型,那么复制返回值比将其包装在引用类型中并稍后解包更快。当您拥有一个getter函数或属性返回基本类型的类字段时,您就会这样做。返回ValueType
也属于性能相同的类别,特别是如果元组的字段本身是原始类型。 - Abion47Tuple
和ValueTuple
的源码。区别在于Tuple
是一个class
,而ValueTuple
是一个实现了IEquatable
的struct
。Tuple == Tuple
将返回false
,但如果它们是相同类型并且包含的每个值的Equals
返回true
,则ValueTuple == ValueTuple
将返回true
。(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";
class MyNonGenericType:MyGenericType <string,ValueTuple,int>
等。 - Jeppe Stig Nielsen针对这两个常识问题,我想补充一点说明:
人们可能会认为批量更改值元组很简单:
Original Answer翻译成"最初的回答"
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