System.ValueTuple和System.Tuple有什么区别?
我反编译了一些C#7库,并使用了ValueTuple
generics。 什么是ValueTuples
,为什么不是Tuple
呢?
什么是
ValuedTuples
,为什么不是Tuple
呢?
ValueTuple
是一个反映元组的结构,与原始的System.Tuple
类相同。
Tuple
和ValueTuple
的主要区别是:
-
System.ValueTuple
是一个值types(结构),而System.Tuple
是一个引用types(class
)。 谈到分配和GC压力时,这是有意义的。 -
System.ValueTuple
不仅是一个struct
,它是一个可变的struct
,而且在使用它的时候一定要小心。 想想当一个类持有一个System.ValueTuple
作为一个字段时会发生什么。 -
System.ValueTuple
通过字段而不是属性暴露它的项目。
直到C#7,使用元组不是很方便。 他们的字段名是Item1
, Item2
等,并且语言没有为大多数其他语言(Python,Scala)提供语法糖。
当.NET语言devise团队决定合并元组并在语言层面添加语法糖时,一个重要的因素就是性能。 由于ValueTuple
是一个值types,因此在使用它时可以避免GC压力,因为(作为实现细节)它们将被分配到堆栈中。
此外,一个struct
得到运行时的自动(浅层)相等语义,而一个class
没有。 尽pipedevise团队确信会有更加优化的元组相等性,因此实现了自定义的相等性。
这里是Tuples
devise笔记的一段:
结构或类:
如前所述,我build议使用元组types
structs
而不是classes
,这样就不会有分配惩罚。 他们应该尽可能轻。可以说,
structs
可能最终成本更高,因为分配复制了更大的价值。 因此,如果他们被分配了比创build更多的东西,那么structs
将是一个不好的select。尽pipe如此,他们的动机是元组短暂的。 当部件比整体更重要时,你会使用它们。 所以常见的模式是构build,返回并立即解构它们。 在这种情况下,结构显然是可取的。
结构还有许多其他的好处,在下面将会变得很明显。
例子:
你可以很容易地看到,使用System.Tuple
很快变得模糊不清。 例如,假设我们有一个计算List<Int>
的总和和计数的方法:
public Tuple<int, int> DoStuff(IEnumerable<int> ints) { 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);
您可以将值元组解构成命名参数的方式是该function的真正威力:
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}");
编译器的好东西:
如果我们看一下前面例子的封面,我们可以看到编译器在parsingValueTuple
时如何解释:
[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
进行注释。 如果我们使用一个新的variables而不是分解,我们得到:
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)); }
请注意,当我们debugging我们的应用程序时,编译器仍然需要做一些魔术(通过属性),因为看到Item1
, Item2
会很奇怪。
Tuple
和ValueTuple
的区别在于Tuple
是一个引用types, ValueTuple
是一个值types。 后者是可取的,因为对C#7中语言的改变使用更频繁的元组,但是在堆上为每个元组分配一个新对象是一个性能问题,特别是在没有必要时。
但是,在C#7中,这个想法是,您不必明确使用任何一种types,因为添加了用于元组使用的语法糖。 例如,在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#之前不经常使用,因为它们很繁琐而且冗长,只有在仅为单个工作实例构build数据类/结构的情况下,才真正用到元组。 但是在C#7中,元组现在拥有语言级别的支持,所以使用它们更清洁,更实用。
我看了Tuple
和ValueTuple
的来源。 区别在于Tuple
是一个class
, ValueTuple
是一个实现IEquatable
的struct
。
这意味着如果它们不是相同的实例,则Tuple == Tuple
将返回false
,但是如果ValueTuple == ValueTuple
的types相同,则它们将返回true
,并且Equals
对它们包含的每个值返回true
。
其他答案忘了提及重要的一点。而不是重新编写,我会参考源代码中的XML文档:
ValueTupletypes(从0到8)构成了C#中的元组和F#中的结构元组的基础的运行时实现。
除了通过语言语法创build ,他们最容易通过ValueTuple.Create
工厂方法创build。 System.ValueTuple
types与System.Tuple
types的不同之处在于:
- 他们是结构而不是阶级,
- 他们是可变的而不是只读的
- 他们的成员(比如Item1,Item2等)是字段而不是属性。
随着这种types和C#7.0编译器的引入,您可以轻松编写
(int, string) idAndName = (1, "John");
并从一个方法中返回两个值:
private (int, string) GetIdAndName() { //..... return (id, name); }
与System.Tuple
相反,您可以更新其成员(可变),因为它们是公有读写字段,可以赋予有意义的名称:
(int id, string name) idAndName = (1, "John"); idAndName.name = "New Name";