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

时间:2016-12-11 08:48:31

标签: c# .net tuples c#-7.0

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

6 个答案:

答案 0 :(得分:163)

  

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

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

TupleValueTuple之间的主要区别是:

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

直到C#7,使用元组不是很方便。他们的字段名称是Item1Item2等,并且语言没有为大多数其他语言提供语法糖(Python,Scala)。

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

此外,struct由运行时获取自动(浅)相等语义,而class则不然。虽然设计团队确保元组的优化更加平等,但是为它实现了自定义相等。

以下是design notes of 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会很奇怪。

答案 1 :(得分:18)

TupleValueTuple之间的区别在于Tuple是引用类型,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#pre-7中经常使用,因为它们既麻烦又冗长,而且只用于仅为单个工作实例构建数据类/结构的情况会比它更麻烦价值。但是在C#7中,元组现在具有语言级支持,因此使用它们更清晰,更有用。

答案 2 :(得分:8)

我查看了TupleValueTuple的来源。不同之处在于TupleclassValueTuple是实现struct的{​​{1}}。

这意味着如果IEquatable不是同一个实例,Tuple == Tuple将返回false,但如果ValueTuple == ValueTuple属于同一类型且true将返回Equals true }}为它们包含的每个值返回 private boolean isDateValid () { boolean flag = false; if ((_day>31) || (_day12) || (_month9999) || (_yearJANUARY) flag = true; break; case 2: if ((_year % 400 == 0) || ((_year % 4 == 0) && (_year % 100 != 0))) { if (_day>FEBRUARY_LEAP) flag = true; } else if (_day>FEBRUARY) flag = true; break; case 3: if (_day>MARCH) flag = true; break; case 4: if (_day>APRIL) flag = true; break; case 5: if (_day>MAY) flag = true; break; case 6: if (_day>JUNE) flag = true; break; case 7: if (_day>JULY) flag = true; break; case 8: if (_day>AUGUST) flag = true; break; case 9: if (_day>SEPTEMBER) flag = true; break; case 10: if (_day>OCTOBER) flag = true; break; case 11: if (_day>NOVEMBER) flag = true; break; case 12: if (_day>DECEMBER) flag = true; break; default: return flag; } return flag; }

答案 3 :(得分:5)

其他答案忘了提到重点。我不打算改写,而是引用instrumentation options of gcc中的XML文档:

ValueTuple类型(从arity 0到8)构成了基础的运行时实现 C#中的元组和F#中的struct元组。

除了通过语言语法创建之外,它们最容易通过 System.ValueTuple工厂方法。 System.Tuple类型与(int, string) idAndName = (1, "John"); 类型不同:

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

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

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

从方法中返回两个值:

System.Tuple

(int id, string name) idAndName = (1, "John"); idAndName.name = "New Name"; 相反,您可以更新其成员(Mutable),因为它们是公共读写字段,可以给出有意义的名称:

  Right     Left     Center     Default
-------     ------ ----------   -------
     12     12        12            12
    123     123       123          123
      1     1          1             1

Table:  Demonstration of simple table syntax.

答案 4 :(得分:5)

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

即。当您通过例如序列化时,您的甜蜜命名参数仍然会以“Item1”,“Item2”等结尾。 Json.NET。

答案 5 :(得分: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

希望这可以帮助某人在列表托管的价值元组中挣扎。