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


139

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


我认为它指的是Dot NEt Tuple类。您能否分享一个示例代码。这样就很容易理解。
Ranadip Dutta

14
@Ranadip Dutta:如果您知道什么是元组,则不需要样本代码即可理解该问题。问题本身很简单:ValueTuple是什么?它与Tuple有何不同?
BoltClock

1
@BoltClock:这就是我在这种情况下没有回答任何问题的原因。我知道在c#中,有一个Tuple类,我经常使用它,有时在powershell中也称它为同一类。它是参考类型。现在看到其他答案,我知道还有一个值类型,也称为Valuetuple。如果有样品,我想知道它的用法。
Ranadip Dutta

2
当Roslyn的源代码在github上可用时,为什么还要对它们进行反编译?
Zein Makki

@ user3185569可能是因为F12的自动反编译的东西,它更容易比跳转到GitHub的
约翰Zabroski

Answers:


203

什么是ValueTuples,为什么不Tuple呢?

A ValueTuple是反映元组的结构,与原始System.Tuple类相同。

Tuple和之间的主要区别ValueTuple是:

  • System.ValueTuple是值类型(结构),System.Tuple而是引用类型(class)。在谈论分配和GC压力时,这是有意义的。
  • System.ValueTuple不仅是一个struct,而且是可变的,在使用它们时一定要小心。想一想当一个类拥有一个System.ValueTupleas字段时会发生什么。
  • System.ValueTuple 通过字段而不是属性公开其项目。

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

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

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

这是以下设计注释中Tuples的一段:

结构或类:

如前所述,我建议创建元组类型structs而不是 classes,这样就不会与它们相关联的分配罚款。它们应尽可能轻巧。

可以说,structs最终可能会导致成本更高,因为分配复制了更大的价值。因此,如果分配给它们的权限远远大于创建的权限,那么structs这将是一个错误的选择。

但是,在其动机中,元组是短暂的。当部分比整体更重要时,您将使用它们。因此,常见的模式是构造,返回并立即对其进行解构。在这种情况下,结构显然是更可取的。

结构还具有许多其他好处,这在下面将变得显而易见。

例子:

您可以轻松地看到与之合作System.Tuple变得非常迅速。例如,假设我们有一种计算a的总和和计数的方法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


1
请注意,您还可以使用更简单(我认为更可取)的语法var (sum, count) = DoStuff(Enumerable.Range(0, 10));
Abion47 '16

@ Abion47如果两种类型不同会怎样?
Yuval Itzchakov '16

您的观点“这是一个可变结构”和“它公开只读字段”又如何一致?
CodesInChaos

@CodesInChaos不是。我看到了[this](github.com/dotnet/corefx/blob/master/src/Common/src/System/…),但是我不认为这是编译器最终发出的结果,因为本地字段无法仍然是只读的。我认为该提案的意思是“您可以根据需要将其设置为只读,但这取决于您”,我对此误解了。
Yuval Itzchakov '16

1
一些提示:“它们将被分配在堆栈上” -仅对局部变量适用。毫无疑问,您知道这一点,但是不幸的是,您所说的方式很可能使值类型始终存在于堆栈中的神话永久化。
彼得·杜尼奥

26

之间的差TupleValueTupleTuple为引用类型和ValueTuple为值类型。后者是可取的,因为在C#7中对语言的更改使元组的使用频率更高,但是为每个元组在堆上分配一个新对象是性能问题,尤其是在不必要时。

但是,在C#7中,想法是您不必显式使用任何一种类型,因为添加了语法糖以供元组使用。例如,在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#7之前的版本中并不经常使用,因为它们笨重且冗长,仅在仅为单个工作实例构建数据类/结构会比其值得多麻烦的情况下才真正使用。但是在C#7中,元组现在具有语言级别的支持,因此使用它们更加简洁和有用。


10

我看了看源都TupleValueTuple。不同的是,TupleclassValueTuplestruct实现IEquatable

认为它的意思Tuple == Tuple将返回false,如果他们是不一样的实例,但ValueTuple == ValueTuple将返回true如果它们是同一类型和Equals回报true每一个它们所包含的值。


不仅如此。
BoltClock

2
@BoltClock如果您要详细说明,您的评论将很有建设性
Peter Morris

3
同样,值类型不一定要放在堆栈上。不同之处在于,无论何时存储该变量(无论是不是堆栈),该语义都表示值,而不是引用。
Servy

6

其他答案忘记了要点。我将代替源代码,而是从源代码中引用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您可以更新其成员(可变),因为它们是公共读写字段,可以赋予有意义的名称:

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

“范围0到8”。啊,我喜欢它们包含一个0元组的事实。它可以用来作为一种空型的,并且将仿制药在被允许不需要某种类型的参数时,像class MyNonGenericType : MyGenericType<string, ValueTuple, int>
叶普斯蒂格尼尔森

6

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

即,当您通过例如Json.NET进行序列化时,您的甜命名参数仍将最终以“ Item1”,“ Item2”等结尾。


2
所以从技术上讲这是相似而不是不同;)
JAD

2

后期加入可以快速阐明这两个事实:

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

有人会认为,不断改变价值元组很简单:

 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'

这种古怪行为的原因是,值元组完全基于值(结构),因此.Select(...)调用适用于克隆结构,而不适用于原始结构。为了解决这个问题,我们必须诉诸:

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

希望这可以帮助某人努力从列表托管的价值元组中脱颖而出。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.