System.Tuple 类型是在.NET 4.0中引入的,但是有两个明显的缺点:
(1) Tuple 类型是引用类型。
(2) 没有构造函数支持。
为了解决这些问题,C# 7 引入了新的语言功能以及新的类型(*)。
现在,如果您需要从函数中返回两个值的合并结果,或者把两个值合并到一个哈希表中,可以使用
类型并使用一个精短的语法来构造它们:
- System.ValueTuple
- // 构建元组实例
- var tpl = (1, 2);
- // 在字典中使用元组
- var d = new Dictionary < (int x, int y),
- (byte a, short b) > ();
- // 不同名称的元组是兼容的
- d.Add(tpl, (a: 3, b: 4));
- // 元组值的语义
- if (d.TryGetValue((1, 2), out
- var r)) {
- // 解构元组忽略第一个元素
- var (_, b) = r;
- // 使用命名语法和定义名称
- Console.WriteLine($ "a: {r.a}, b: {r.Item2}");
- }
(*)
类型在.NET Framework 4.7中引入。但是您仍然可以在较低的框架版本中使用这个功能,这时候,您必须引用一个特殊的nuget包:System.ValueTuple。
- System.ValueTuple
。
- (Type1 name1, Type2 name2)
。
- (value1, optionalName: value2)
。
- (int a, int b) = (1, 2)
、
- (1,2).Equals((a: 1, b: 2))
返回的值均是
- (1,2).GetHashCode() == (1,2).GetHashCode()
。
- true
和
- ==
。在github上有一个悬而未决的讨论:“支持==和!=元组类型”。
- !=
语句中转换:
- case
- OK,
- var (x, y) = (1,2)
- OK,
- (var x, int y) = (1,2)
- not OK,
- dictionary.TryGetValue(key, out var (x, y))
- not OK。
- case var (x, y): break;
.
- (int a, int b) x = (1,2); x.a++;
、
- Item1
等来访问。
- Item2
(**) 我们马上就会明白上面几点。
缺少用户定义的名称导致
类型不常用。我们可以将
- System.Tuple
用作一个精减方法的实现细节,但如果我们需要传递它,我更喜欢使用具有描述性属性名称的命名类型。新元组功能很好地解决了这个问题:可以为元组元素指定名称,而不像匿名类型,即使在不同的程序集中也可以使用这些名称。
- System.Tuple
C#编译器为方法签名中使用的每个元组类型指定了一个特殊的标记TupleElementNamesAttribute(***) :
(***)
标记非常特殊,不能在用户代码中直接使用。如果您尝试使用它,编译器会报出错误。
- TupleElementNamesAttribute
- public (int a, int b) Foo1((int c, int d) a) => a;
- [return: TupleElementNames(new[] { "a", "b" })]
- public ValueTuple<int, int> Foo(
- [TupleElementNames(new[] { "c", "d" })] ValueTuple<int, int> a)
- {
- return a;
- }
这有助于IDE和编译器“检查”元素名称,并警告错误地使用它们:
- // 正确: 元组声明可以跳过元素名称
- (int x, int y) tpl = (1, 2);
- // 警告: 由于目标类型“(int x, int y)”指定了其他名称或未指定名称,因此元组元素名称“a”被忽略。
- tpl = (a: 1, b: 2);
- // 正确 :元组解构忽略元素名称
- var (a, b) = tpl;
- // x: 2, y: 1. 元组名被忽略
- var (y, x) = tpl;
编译器对继承的成员有较强的要求:
- public abstract class Base
- {
- public abstract (int a, int b) Foo();
- public abstract (int, int) Bar();
- }
- public class Derived : Base
- {
- // 错误:替代继承成员“Base.Foo()”时无法更改元组元素名称
- public override (int c, int d) Foo() => (1, 2);
- // 错误:替代继承成员“Base.Bar()”时无法更改元组元素名称
- public override (int a, int b) Bar() => (1, 2);
- }
常规方法参数可以在重写成员中自由更改,重写成员中的元组元素名称应该与基本类型中的元素名称完全匹配。
C# 7.1 引入了一个额外的增强功能:元素名称推断类似于C#为匿名类型所做的推断。
- public void NameInference(int x, int y)
- {
- // (int x, int y)
- var tpl = (x, y);
- var a = new {X = x, Y = y};
- // (int X, int Y)
- var tpl2 = (a.X, a.Y);
- }
元组是公共字段可变的值类型。这听起来令人担忧,因为我们知道可变值类型被认为是有害的。这是一个邪恶的小例子:
- var x = new { Items = new List<int> { 1, 2, 3 }.GetEnumerator() };
- while (x.Items.MoveNext())
- {
- Console.WriteLine(x.Items.Current);
- }
如果运行这个代码,您会得到一个无限循环。
是一个可变值类型,但是
- List<T>.Enumerator
是属性。这意味着
- Items
在每个循环迭代中返回原始迭代器的副本,从而导致无限循环。
- x.Items
但是只有当数据与行为混合在一起时,可变值类型才是危险的:枚举器拥有一个状态(当前元素)并具有行为(通过调用MoveNext方法来推进迭代器的能力)。这种组合可能会导致问题,因为在副本上调用方法而不是在原始实例上调用方法,从而导致无效操作。下面是一组由于值类型的隐藏副本而导致不明显行为的示例:gist。
但可变性问题依然存在:
- var tpl = (x: 1, y: 2);
- var hs = new HashSet<(int x, int y)>();
- hs.Add(tpl);
- tpl.x++;
- Console.WriteLine(hs.Contains(tpl)); // false
元组在字典中作为键是非常有用的,并且由于适当的值语义可以存储在哈希表中。但是您不应该在集合的不同操作之间改变一个元组变量的状态。
虽然元组的构造函数对于元组来说非常特殊的,但是解构非常通用,并且可以与任何类型一起使用。
- public static class VersionDeconstrucion
- {
- public static void Deconstruct(this Version v, out int major, out int minor, out int build, out int revision)
- {
- major = v.Major;
- minor = v.Minor;
- build = v.Build;
- revision = v.Revision;
- }
- }
- var version = Version.Parse("1.2.3.4");
- var (major, minor, build, _) = version;
- // Prints: 1.2.3
- Console.WriteLine($"{major}.{minor}.{build}");
解构使用“鸭子类型(duck-typing)”的方法:如果编译器可以找到一个方法调用
给定的类型 - 实例方法或扩展方法 - 类型即是可解构的。
- Deconstruct
一旦您开始使用元组,很快就会意识到想在源代码的多个地方“重用”一个元组类型,但这并没有什么问题。首先,虽然C#不支持给定类型的全局别名,不过您可以使用“using”别名指令,它会在一个文件中创建一个别名;其次,您不能将元组指定别名:
- //您不能这样做:编译错误
- using Point = (int x, int y);
- // 但是您可以这样做
- using SetOfPoints = System.Collections.Generic.HashSet < (int x, int y) > ;
github上有一个关于“使用指令中的元组类型”的讨论。所以,如果您发现自己在多个地方使用一个元组类型,你有两个选择:保持复制粘贴或创建一个命名的类型。
下面是一个有趣的问题:我们应该遵循什么命名规则来处理元组元素?Pascal规则喜欢
还是骆峰规则
- ElementName
?一方面,元组元素应该遵循公共成员的命名规则(即PascalCase),但另一方面,元组只是包含变量的变量,变量应该遵循骆峰规则。
- elementName
如果元组被用作参数或方法的返回类型使用
规则,并且如果在函数中本地创建元组使用
- PascalCase
规则,可以考虑使用基于用法和使用的不同命名方案。但我更喜欢总是使用
- camelCase
。
- camelCase
我发现元组在日常工作中非常有用。我需要不止一个函数返回值,或者我需要把一对值放入一个哈希表,或者字典的Key非常复杂,我需要用另一个“字段”来扩展它。
我甚至使用它们来避免与方法类似的ConcurrentDictionary.TryGetOrAdd的闭包分配,需要额外的参数。在许多情况下,状态也是一个元组。
该功能是非常有用的,但我还想看到一些增强功能:
、
- out var
语法。
- case var
进行相等比较。
- ==
(****)我知道这些功能是有争议的,但我认为它非常有用的。我们可以等待Record类型,但还不确定Record是值类型还是引用类型。
来源: http://www.cnblogs.com/tdfblog/p/dissecting-the-tuples-in-c-7.html