本文要点

  • 应遵循《.NET设计规范:.NET约定惯用法与模式》一书。和十年前第一版出版时一样,书中给出的原则在当前依然有指导意义。
  • API设计是最重要的。设计不好的API会在极大地增加软件缺陷,同时降低可重用性。
  • 时刻牢记“良性循环”(Pit of Success)这一哲理:让正确的事情更易于做,让犯错误更加困难。
  • 移除“线路噪音”(Line Noise)和“样板”(Boilerplate)代码,聚焦于对业务逻辑的关注。
  • 出于性能考虑而牺牲代码清晰度前,请认真考虑一下。

C# 7是一个重大更新,其中提供了很多有意思的新功能。虽然已有大量的文章介绍这些功能可以做什么,但是鲜有文章介绍应如何使用这些功能。本文将过一遍《.NET设计规范:.NET约定惯用法与模式》 (译者注:英文书名为“Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries”)一书中给出的指导原则,力图更好地使用C# 7的新特性。

元组返回(Tuple Returns)

通常在C#编程中,一个函数返回多个值实现起来十分繁琐。一种做法是使用输出参数,这只适用于暴露异步方法的情况。另一种做法是使用 Tuple<T>。创建Tuple<T>过于啰嗦,需要做内存分配,并且Tuple的字段没有描述性名字。也可以使用自定义的结 构体。虽然结构体在性能上要优于元组,但是大量使用一次性类型会将代码弄得一团糟。而使用具有动态特性的匿名类型,存在性能不好的问题,还缺少静态类型检 查。

在C# 7中新提供了元组返回语法,它解决了全部上述问题。下面给出一个基本语法的例子:

public (string, string) LookupName(long id) // tuple return type
{
return ("John", "Doe"); //元组常值。
}
var names = LookupName();
var firstName = names.Item1;
var lastName = names.Item2;

该函数的实际返回类型是ValueTuple<string, string>。正如名称所示,ValueTuple<string, string>类似于Tuple<T>类,是一个轻量级的结构体。它解决了类型膨胀(Type Bloat)问题,但是依然没有解决描述性名称这一困扰Tuple<T>的问题。我们看一下如下的例子:

public (string First, string Last) LookupName(long id)
var names = LookupName(0);
var firstName = names.First;
var lastName = names.Last;

其中的返回类型依然是ValueTuple<string, string>,但是现在编译器在函数中添加了一个TupleElementNames属性。这样调用该函数的代码就可以使用描述性名称,而不再是Item1或Item2这样的名称了。

警告: TupleElementNames属性只能由编译器赋予。如果返回类型上使用了反射,你将只能看到裸的ValueTuple<T>结构体。因为在获得结果时,属性是位于函数本身上,而这个信息丢失了。

编译器会尽可能维护额外类型的幻象。例如,给出如下这些声明:

var a = LookupName(0);
(string First, string Last) b = LookupName(0);
ValueTuple<string, string> c = LookupName(0);
(string make, string model) d = LookupName(0);

在编译器看来,a和b同是(string First, string Last)。鉴于c被显式声明为ValueTuple<string, string>,因此不存在c.First属性。

该例中d的赋值语句展示了这一设计的失灵之处,即会在一定程度上导致缺失类型安全。字段意外地重命名是一个非常容易发生的问题,一个元组可以错误地 指定给另一个恰好具有同样形状的元组。这同样是由于编译器没有真正地将(string First, string Last)和(string make, string model)区分为不同的类型。

ValueTuple是可变的

有意思的是, ValueTuple是可变的。Mads Torgersen给出了这样的解释:

为什么通常可变结构体是不好的,不要应用于元组?下面给出原因。

如果你按正常的封装方式编写了一个可变结构体,并且其中具有私有的状态,还有公开的修改器(Mutator)属性和方法,那么你可能就会陷入一些严重的错误中。因为只要结构体是保持在只读变量中,那么修改器就会默默地工作于结构体的一个拷贝上!

但是元组的确有公开的可变字段。它在设计上并未考虑修改器,因此不存在出现上述现象的风险。

此外,ValueTuple是结构体,而结构体在传递时需要进行拷贝。结构体并不直接在线程间共享,也不承担“共享可变状态”的风险。这不同于System.Tuple家族的类型,这些类型也是类。为确保线程安全,需要这些类型是不可变的。

注意,这里Torgersen所指的是“字段”,而不是“属性”。对于使用元组返回函数结果的反射库,这会导致问题。

元组返回的指导原则

  • 当字段列表规模较小并不会发生更改时,考虑使用元组返回,而不是out参数。
  • 对元组返回中的描述性名字使用帕斯卡拼写法(PascalCase),这会使得元组字段看上去就像是正常的类和结构中的属性。
  • 在不进行解析就读取元组返回时,使用var,以避免意外地误标字段。
  • X 如果想要对返回值使用反射,应避免返回值元组。
  • X 如果在未来的版本中可能会返回额外的字段,那么就不要在公开API上使用元组返回。在元组返回中添加字段是一种破坏性变更。

----------------------------------------------------节选自infoQ:C# 7编程模式与实践-----------------------------------------------------------------

C# 7中函数多值返回_转自InfoQ的更多相关文章

  1. Go中函数作为值、类型传递。

    在Go中函数也是一种变量,我们可以通过type来定义它,它的类型就是所有拥有相同的参数,相同的返回值的一种类型 type typeName func(input1 inputType1 , input ...

  2. JavaScript中函数作为值

    function myfunc() { // .. } 这是个函数,这样理解, myfunc只是外层作用域的一个变量,指向刚刚声明的function. 也就是说,function本身就是一个值, 就像 ...

  3. java中函数是值传递还是引用传递?

    相信有些同学跟我一样,曾经对这个问题很疑惑.在网上也看了一些别人说的观点,评论不一.有说有值传递和引用传递两种,也有说只有值传递的,这里只说下个人见解 先看一个例子 public class Test ...

  4. [转]Python中函数的值传递和引用传递

    首先还是应该科普下函数参数传递机制,传值和传引用是什么意思? 函数参数传递机制问题在本质上是调用函数(过程)和被调用函数(过程)在调用发生时进行通信的方法问题.基本的参数传递机制有两种:值传递和引用传 ...

  5. 2013年6月19日星期三java中函数地址值传递

    今天代码审核时确认了一个问题,理解了java中string和stringbuffer赋值问题,看到一个帖子很好,摘录如下: 理解这两个例子需要分清实参和形参的区别,引用和对象的区别 第一个例子的内部执 ...

  6. 《挑战30天C++入门极限》C++运算符重载函数基础及其值返回状态

        C++运算符重载函数基础及其值返回状态 运算符重载是C++的重要组成部分,它可以让程序更加的简单易懂,简单的运算符使用可以使复杂函数的理解更直观. 对于普通对象来说我们很自然的会频繁使用算数运 ...

  7. c语言中函数的简单介绍

    c语言中函数的介绍: 函数,简单的说就是代码的打包.存放在一个地方,当需要的时候调用. 函数分类: 1.无参无返回值函数 void func() 2.无参有返回值函数  int func() 3.有参 ...

  8. C语言中函数和指针的參数传递

    近期写二叉树的数据结构实验.想用一个没有返回值的函数来创建一个树,发现这个树就是建立不起来,那么我就用这个样例讨论一下c语言中指针作为形參的函数中传递中隐藏的东西. 大家知道C++中有引用的概念,两个 ...

  9. C#中 多线程执行含有返回值的函数

    C# 中,传统的多线程并不支持多线程执行含有返回结果的函数.虽然可以通过制作外壳类来使得返回结果得以保留,但如果一定时间内函数未执行完,简单的外壳类可能就无法满足需求了. class netHelpe ...

随机推荐

  1. Nowcoder 提高组练习赛-R1

    https://www.nowcoder.com/acm/contest/172#question 单人报名300元,五人合报免费,于是就和学弟同学学长们组了一个三世同堂的队伍,高一的学长wzhqwq ...

  2. 不要以为字段以transient修饰的话就一定不会被序列化

    1: 先阅读这边文章:http://www.importnew.com/21517.html 2:被transient修饰真的会被序列化吗? 反例:java.util.ArrayList中底层存储数组 ...

  3. Shell学习心得(二):传递参数、运算符

    1.传递参数 可以在执行 Shell 脚本时,向脚本传递参数,脚本内获取参数的格式为:$n.n 代表一个数字,1 为执行脚本的第一个参数,2 为执行脚本的第二个参数,以此类推…… 向脚本传递三个参数, ...

  4. connection reset by peer问题总结及解决方案

    找遍了 中英文网站,翻遍了能找的角落,发现了出现故障的原因和原理,及改如何处理,这里记录下,希望能帮助到有需要的小伙伴,少走点弯路, 以上就整理内容: connection reset by peer ...

  5. [转]OPENCV3.3+CUDA9.0 环境搭建若干错误总结

    编译OpenCV设计启用OpenGL三维可视化支持和启用GPU CUDA并行加速处理的基本知识: 1.从2.4.2版本开始,OpenCV在可视化窗口中支持OpenGL,这就意味着在OpenCV中可以轻 ...

  6. VMware 克隆多台Linux机器并配置IP

    1.查看并分配虚拟网络 我们首先要知道 VMware 三种网络模式的区别. ①.Bridged(桥接模式):就是将主机网卡与虚拟机虚拟的网卡利用虚拟网桥进行通信.在桥接的作用下,类似于把物理主机虚拟为 ...

  7. 详细解读大数据分析引擎Pig&PigLatin语句

    Pig 一.Pig的介绍: Pig由Yahoo开发,主要应用于数据分析,Twitter公司大量使用Pig处理海量数据,Pig之所以是数据分析引擎,是因为Pig相当于一个翻译器,将PigLatin语句翻 ...

  8. 带您详细解读分布式文件系统HDFS

    一.HDFS的由来: 本地系统:一个节点作为系统,以前数据是存放在本地文件系统上的,但本地文件系统存在两个问题:1.本地节点存储容量不够大:2.本地节点会坏,数据不够安全.这时,人们开始利用闲置的计算 ...

  9. NanoPC-T2制作刷机包

    anoPC-T2制作刷机包 前提:到友善的wiki中,仔细看编译uboot.内核.制作刷机包的教程. 准备工作: 1. 虚拟机Ubuntu安装,并安装n多软件可以支撑编译内核等等. 2.  安装交叉编 ...

  10. 一段程序的分析——C++析构器,何时析构

    最近在看小甲鱼的视频,有段程序是这么写的: #include <iostream> #include <string> class Pet { public: Pet(std: ...