System.Tuple 类型是在.NET 4.0中引入的,但是有两个明显的缺点:

(1) Tuple 类型是引用类型。

(2) 没有构造函数支持。

为了解决这些问题,C# 7 引入了新的语言功能以及新的类型(*)。

现在,如果您需要从函数中返回两个值的合并结果,或者把两个值合并到一个哈希表中,可以使用System.ValueTuple类型并使用一个精短的语法来构造它们:

  1. // 构建元组实例
  2. var tpl = (1, 2);
  3. // 在字典中使用元组
  4. var d = new Dictionary<(int x, int y), (byte a, short b)>();
  5. // 不同名称的元组是兼容的
  6. d.Add(tpl, (a: 3, b: 4));
  7. // 元组值的语义
  8. if (d.TryGetValue((1, 2), out var r))
  9. {
  10. // 解构元组忽略第一个元素
  11. var (_, b) = r;
  12. // 使用命名语法和定义名称
  13. Console.WriteLine($"a: {r.a}, b: {r.Item2}");
  14. }

(*) System.ValueTuple 类型在.NET Framework 4.7中引入。但是您仍然可以在较低的框架版本中使用这个功能,这时候,您必须引用一个特殊的nuget包: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上有一个悬而未决的讨论:“支持==和!=元组类型”
  • 元组可以被“解构”,但只能转换成“变量声明”,而不能“out var”或case语句中转换:var (x, y) = (1,2) - OK, (var x, int y) = (1,2) - OK, dictionary.TryGetValue(key, out var (x, y)) - not OK, case var (x, y): break; - not OK。
  • 元组是可变的:(int a, int b) x = (1,2); x.a++;.
  • 元组元素可以通过名称(如果提供的话)或通过通用名称Item1Item2等来访问。

(**) 我们马上就会明白上面几点。

元组名称

缺少用户定义的名称导致System.Tuple类型不常用。我们可以将System.Tuple用作一个精减方法的实现细节,但如果我们需要传递它,我更喜欢使用具有描述性属性名称的命名类型。新元组功能很好地解决了这个问题:可以为元组元素指定名称,而不像匿名类型,即使在不同的程序集中也可以使用这些名称。

C#编译器为方法签名中使用的每个元组类型指定了一个特殊的标记TupleElementNamesAttribute(***) :

(***)TupleElementNamesAttribute标记非常特殊,不能在用户代码中直接使用。如果您尝试使用它,编译器会报出错误。

  1. public (int a, int b) Foo1((int c, int d) a) => a;
  2. [return: TupleElementNames(new[] { "a", "b" })]
  3. public ValueTuple<int, int> Foo(
  4. [TupleElementNames(new[] { "c", "d" })] ValueTuple<int, int> a)
  5. {
  6. return a;
  7. }

这有助于IDE和编译器“检查”元素名称,并警告错误地使用它们:

  1. // 正确: 元组声明可以跳过元素名称
  2. (int x, int y) tpl = (1, 2);
  3. // 警告: 由于目标类型“(int x, int y)”指定了其他名称或未指定名称,因此元组元素名称“a”被忽略。
  4. tpl = (a:1, b:2);
  5. // 正确 :元组解构忽略元素名称
  6. var (a, b) = tpl;
  7. // x: 2, y: 1. 元组名被忽略
  8. var (y, x) = tpl;

编译器对继承的成员有较强的要求:

  1. public abstract class Base
  2. {
  3. public abstract (int a, int b) Foo();
  4. public abstract (int, int) Bar();
  5. }
  6. public class Derived : Base
  7. {
  8. // 错误:替代继承成员“Base.Foo()”时无法更改元组元素名称
  9. public override (int c, int d) Foo() => (1, 2);
  10. // 错误:替代继承成员“Base.Bar()”时无法更改元组元素名称
  11. public override (int a, int b) Bar() => (1, 2);
  12. }

常规方法参数可以在重写成员中自由更改,重写成员中的元组元素名称应该与基本类型中的元素名称完全匹配。

元素名称推断

C# 7.1 引入了一个额外的增强功能:元素名称推断类似于C#为匿名类型所做的推断。

  1. public void NameInference(int x, int y)
  2. {
  3. // (int x, int y)
  4. var tpl = (x, y);
  5. var a = new {X = x, Y = y};
  6. // (int X, int Y)
  7. var tpl2 = (a.X, a.Y);
  8. }

值语义和可变性

元组是公共字段可变的值类型。这听起来令人担忧,因为我们知道可变值类型被认为是有害的。这是一个邪恶的小例子:

  1. var x = new { Items = new List<int> { 1, 2, 3 }.GetEnumerator() };
  2. while (x.Items.MoveNext())
  3. {
  4. Console.WriteLine(x.Items.Current);
  5. }

如果运行这个代码,您会得到一个无限循环。List&lt;T&gt;.Enumerator是一个可变值类型,但是Items是属性。这意味着x.Items在每个循环迭代中返回原始迭代器的副本,从而导致无限循环。

但是只有当数据与行为混合在一起时,可变值类型才是危险的:枚举器拥有一个状态(当前元素)并具有行为(通过调用MoveNext方法来推进迭代器的能力)。这种组合可能会导致问题,因为在副本上调用方法而不是在原始实例上调用方法,从而导致无效操作。下面是一组由于值类型的隐藏副本而导致不明显行为的示例:gist

但可变性问题依然存在:

  1. var tpl = (x: 1, y: 2);
  2. var hs = new HashSet<(int x, int y)>();
  3. hs.Add(tpl);
  4. tpl.x++;
  5. Console.WriteLine(hs.Contains(tpl)); // false

元组在字典中作为键是非常有用的,并且由于适当的值语义可以存储在哈希表中。但是您不应该在集合的不同操作之间改变一个元组变量的状态。

解构

虽然元组的构造函数对于元组来说非常特殊的,但是解构非常通用,并且可以与任何类型一起使用。

  1. public static class VersionDeconstrucion
  2. {
  3. public static void Deconstruct(this Version v, out int major, out int minor, out int build, out int revision)
  4. {
  5. major = v.Major;
  6. minor = v.Minor;
  7. build = v.Build;
  8. revision = v.Revision;
  9. }
  10. }
  11. var version = Version.Parse("1.2.3.4");
  12. var (major, minor, build, _) = version;
  13. // Prints: 1.2.3
  14. Console.WriteLine($"{major}.{minor}.{build}");

解构使用“鸭子类型(duck-typing)”的方法:如果编译器可以找到一个方法调用Deconstruct给定的类型 - 实例方法或扩展方法 - 类型即是可解构的。

元组别名

一旦您开始使用元组,很快就会意识到想在源代码的多个地方“重用”一个元组类型,但这并没有什么问题。首先,虽然C#不支持给定类型的全局别名,不过您可以使用“using”别名指令,它会在一个文件中创建一个别名;其次,您不能将元组指定别名:

  1. //您不能这样做:编译错误
  2. using Point = (int x, int y);
  3. // 但是您可以这样做
  4. using SetOfPoints = System.Collections.Generic.HashSet<(int x, int y)>;

github上有一个关于“使用指令中的元组类型”的讨论。所以,如果您发现自己在多个地方使用一个元组类型,你有两个选择:保持复制粘贴或创建一个命名的类型。

命名规则

下面是一个有趣的问题:我们应该遵循什么命名规则来处理元组元素?Pascal规则喜欢ElementName还是骆峰规则elementName?一方面,元组元素应该遵循公共成员的命名规则(即PascalCase),但另一方面,元组只是包含变量的变量,变量应该遵循骆峰规则。

如果元组被用作参数或方法的返回类型使用PascalCase规则,并且如果在函数中本地创建元组使用camelCase规则,可以考虑使用基于用法和使用的不同命名方案。但我更喜欢总是使用camelCase

总结

我发现元组在日常工作中非常有用。我需要不止一个函数返回值,或者我需要把一对值放入一个哈希表,或者字典的Key非常复杂,我需要用另一个“字段”来扩展它。

我甚至使用它们来避免与方法类似的ConcurrentDictionary.TryGetOrAdd的闭包分配,需要额外的参数。在许多情况下,状态也是一个元组。

该功能是非常有用的,但我还想看到一些增强功能:

  1. 全局别名:能够“命名”一个元组并在整个程序集中使用它们(****)。
  2. 在模式匹配中解构一个元组:out varcase var语法。
  3. 使用运算符==进行相等比较。

(****)我知道这些功能是有争议的,但我认为它非常有用的。我们可以等待Record类型,但还不确定Record是值类型还是引用类型。

原文:《Dissecting the tuples in C# 7》https://blogs.msdn.microsoft.com/seteplia/2017/11/01/dissecting-the-tuples-in-c-7/

翻译:Sweet Tang

本文地址:http://www.cnblogs.com/tdfblog/p/dissecting-the-tuples-in-c-7.html

欢迎转载,请在明显位置给出出处及链接。

解析 C# 7中的元组类型(ValueTuple)的更多相关文章

  1. 在Java中使用元组类型的利器

    Java本身并没有内置元组这一项特性,要使用元组必须自行实现,所幸现在这些编程语言都支持泛型, 实现非内置元组也变的异常简单, 但是毕竟是非语言内置的语法元素,使用起来肯定不如原生元组来的便捷. 下面 ...

  2. python-函数中的参数类型和可变参数解析

    最近,在学习python,天天抱着廖雪峰的教材苦练,https://www.liaoxuefeng.com/ 但廖老毕竟是如此的才华盈溢,我这等小辈真是读起来教程都有些费力. 关于python-函数中 ...

  3. Python中的集合类型分类和集合类型操作符解析

    集合类型    数学上,把set称作由不同的元素组成的集合,集合(set)的成员通常被称作集合元素(set elements).    Python把这个概念引入到它的集合类型对象里.集合对象是一组无 ...

  4. 解析MySQL中存储时间日期类型的选择问题

    解析MySQL中存储时间日期类型的选择问题_Mysql_脚本之家 https://www.jb51.net/article/125715.htm 一般应用中,我们用timestamp,datetime ...

  5. python元组类型

    元组类型简介 使用括号包围的数据结构是元组(tuple).例如: >>> (1,2,3) (1, 2, 3) >>> T = (1,2,3,) >>&g ...

  6. C#中的元组对象Tuple

    原文:C#中的元组对象Tuple 一.什么是元组 元组就是一些对象的集合,在我们编程时,比如一个人的信息,我们常常创建一个Person类去描述一个人,传统的做法如下: public class Per ...

  7. C# 9.0元组 (ValueTuple)详细解说

    元组 (ValueTuple)类型是值类型:元组元素是公共字段,可以使用任意数量的元素定义元组.Tuple类型像一个口袋,在出门前可以把所需的任何东西一股脑地放在里面.您可以将钥匙.驾驶证.便笺簿和钢 ...

  8. Python中的鸭子类型

    今天,我们来聊一聊Python中的鸭子类型(duck typing). 编程语言具有类型概念,例如Python中有数字类型.字符串类型.布尔类型,或者更加复杂的结构,例如元组tuple.列表list. ...

  9. Python中内置数据类型list,tuple,dict,set的区别和用法

    Python中内置数据类型list,tuple,dict,set的区别和用法 Python语言简洁明了,可以用较少的代码实现同样的功能.这其中Python的四个内置数据类型功不可没,他们即是list, ...

随机推荐

  1. 尝试在Linux上部署Asp.net Core应用程序

    快两个月没接触.net,倒是天天在用Linux,所以想尝试一下在Linux运行喜欢的.net 应用. 安装CentOS 安装.Net core for Linux 创建Asp.net Core应用程序 ...

  2. 【京东详情页】——原生js爬坑之标签页

    一.引言 要做详情页的商品评价等5个li的标签页转换,效果如下: 二.实现原理 有一个特别的地方:上面五个li,但下面只有四个容器(table/div). 设计的目的:无论点哪个li,只有前四个div ...

  3. MongDB开启权限认证

    在生产环境中MongoDB已经使用有一段时间了,但对于MongoDB的数据存储一直没有使用到权限访问(MongoDB默认设置为无权限访问限制),最近在酷壳网看了一篇技术文章(https://cools ...

  4. JS -- Variables As Properties

    Variables As Properties When you declare a global JavaScript variable, what you are actually doing i ...

  5. 洗礼灵魂,修炼python(5)--python操作符,内置函数

    前面提到了BIF(内置函数)这个概念,什么是内置函数,就是python已经定义好的函数,不需要人为再自己定义,直接拿来就可以用的函数,那么都有哪些BIF呢? 可以在交互式界面(IDLE)输入这段代码, ...

  6. ClassLoader类加载机制&&JVM内存管理

    一.ClassLoader类加载机制 在java中类加载是遵循委派双亲加载的:通过调用loadClass方法逐级往上传递委派加载请求,当找不到父ClassLoader时调用其findClass方法尝试 ...

  7. VB.NET生成重复窗体

    Public Class Form1 Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click ...

  8. HDU1411 欧拉四面体

    用向量解决: 三角形面积:S=1/2*|x1*y2-x2*y1|;      (粗体表示向量) 三棱锥体积:V=1/6*(OA*OB)*OC 不知道哪里去找的代码,毕竟很线性代数矩阵什么的很头疼,晚上 ...

  9. 数据库的优化(表优化和sql语句优化)

    在这里主要是分为表设计优化和sql语句优化两方面来实现. 首先的是表设计优化: 1.数据行的长度不要超过8020字节.如果是超过这个长度的话这条数据会占用两行,减低查询的效率. 2.能用数字类型就不要 ...

  10. ASP.NET没有魔法——ASP.NET MVC是如何运行的?它的生命周期是什么?

    前面的文章我们使用ASP.NET MVC创建了个博客应用,那么它是如何工作的呢?我们都知道ASP.NET的程序需要部署到IIS上才能够通过浏览器来访问,那么IIS与ASP.NET MVC程序之间又是如 ...