System.ValueTuple 和 System.Tuple 有什么区别?

What's the difference between System.ValueTuple and System.Tuple?

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

What are ValueTuples and why not Tuple instead?

A ValueTuple 是一个反映元组的结构,与原始 System.Tuple class.

相同

TupleValueTuple的主要区别是:

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

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

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

此外,struct 会在运行时获得自动(浅层)相等语义,而 class 则不会。尽管设计团队确保元组的相等性会更加优化,因此为其实现了自定义相等性。

这是 design notes of Tuples 中的一段话:

Struct or Class:

As mentioned, I propose to make tuple types structs rather than classes, so that no allocation penalty is associated with them. They should be as lightweight as possible.

Arguably, structs can end up being more costly, because assignment copies a bigger value. So if they are assigned a lot more than they are created, then structs would be a bad choice.

In their very motivation, though, tuples are ephemeral. You would use them when the parts are more important than the whole. So the common pattern would be to construct, return and immediately deconstruct them. In this situation structs are clearly preferable.

Structs also have a number of other benefits, which will become obvious in the following.


示例:

您可以很容易地看到使用 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.[=53= 会很奇怪]

我查看了 TupleValueTuple 的来源。区别在于 Tuple 是一个 classValueTuple 是一个 struct 实现了 IEquatable.

这意味着如果它们不是同一个实例,Tuple == Tuple 将 return false,但如果它们不是同一实例,ValueTuple == ValueTuple 将 return true它们属于同一类型,并且对于它们包含的每个值 Equals returns true

TupleValueTuple的区别在于Tuple是引用类型,ValueTuple是值类型。后者是可取的,因为 C# 7 中语言的变化使得元组的使用更加频繁,但是在堆上为每个元组分配一个新对象是一个性能问题,尤其是在不必要的时候。

但是,在 C# 7 中,您永远不必显式使用任何一种类型,因为为元组使用添加了语法糖。例如,在 C# 6 中,如果要使用元组来 return 一个值,则必须执行以下操作:

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 之前的版本中并不经常使用,因为它们既麻烦又冗长,并且仅在为单个工作实例构建数据 class/struct 比这是值得的。但在 C# 7 中,元组现在具有语言级支持,因此使用它们更加简洁和有用。

其他答案忘记提及重要的 points.Instead 改写,我将参考 source code:

中的 XML 文档

ValueTuple 类型(从元数 0 到 8)构成了作为底层的运行时实现 C# 中的元组和 F# 中的结构元组。

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

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

引入此类型和C# 7.0编译器,您可以轻松编写

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

和return来自一个方法的两个值:

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

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

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

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

即当通过例如序列化时,您的甜美命名参数仍将以 "Item1"、"Item2" 等结尾。 Json.NET.

Late-joining 添加对这两个事实的快速说明:

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

有人会认为更改 value-tuples en-masse 会很简单:

 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'

这种古怪行为的原因是 value-tuples 恰好是 value-based(结构),因此 .Select(...) 调用适用于 cloned-structs 而不是在原件上。要解决这个问题,我们必须求助于:

 // 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

希望这可以帮助那些努力从 list-hosted value-tuples.

中找出正面和反面的人