我反编译了一些C#7库,并看到了ValueTuple
泛型。什么是ValueTuples
,为什么不Tuple
呢?
我反编译了一些C#7库,并看到了ValueTuple
泛型。什么是ValueTuples
,为什么不Tuple
呢?
Answers:
什么是
ValueTuples
,为什么不Tuple
呢?
A ValueTuple
是反映元组的结构,与原始System.Tuple
类相同。
Tuple
和之间的主要区别ValueTuple
是:
System.ValueTuple
是值类型(结构),System.Tuple
而是引用类型(class
)。在谈论分配和GC压力时,这是有意义的。System.ValueTuple
不仅是一个struct
,而且是可变的,在使用它们时一定要小心。想一想当一个类拥有一个System.ValueTuple
as字段时会发生什么。System.ValueTuple
通过字段而不是属性公开其项目。在C#7之前,使用元组还不是很方便。它们的字段名称是Item1
,Item2
等,并且该语言没有像大多数其他语言(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;
}
在内部,编译后的代码使用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));
之间的差Tuple
和ValueTuple
是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#7之前的版本中并不经常使用,因为它们笨重且冗长,仅在仅为单个工作实例构建数据类/结构会比其值得多麻烦的情况下才真正使用。但是在C#7中,元组现在具有语言级别的支持,因此使用它们更加简洁和有用。
我看了看源都Tuple
和ValueTuple
。不同的是,Tuple
是class
和ValueTuple
是struct
实现IEquatable
。
认为它的意思Tuple == Tuple
将返回false
,如果他们是不一样的实例,但ValueTuple == ValueTuple
将返回true
如果它们是同一类型和Equals
回报true
每一个它们所包含的值。
其他答案忘记了要点。我将代替源代码,而是从源代码中引用XML文档。:
ValueTuple类型(从0到8)在运行时实现中,它是C#中的元组和F#中的结构元组的基础。
除了通过语言语法创建之外,还可以通过ValueTuple.Create
工厂方法最轻松地创建它们
。这些System.ValueTuple
类型与以下类型不同System.Tuple
:
通过引入这种类型和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";
class MyNonGenericType : MyGenericType<string, ValueTuple, int>
等
后期加入可以快速阐明这两个事实:
有人会认为,不断改变价值元组很简单:
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
希望这可以帮助某人努力从列表托管的价值元组中脱颖而出。