• 2016年8月23日,Android 7.0(N)正式通过OTA向Nexus 5X/6P 用户推送啦
  • 2016年8月3日,Windows 10 周年版已经可以通过系统更新来升级了,更加人性化的功能等你发现
  • 美Win网的朋友,现在可以使用QQ号和微博帐号来快速登录了,赶紧参与到大家的讨论中来吧!
  • 美Win网问答社区现在已经正式开放了,有关Windows 系统的任何疑问都可以在问答中心提问,当然你也可以参与回答其他网友的问题!

微软:Visual Studio 2017之C# 7.0新功能介绍

Microsoft iMeiwin 来源:原创 1672次浏览 1个评论

这篇文章介绍了 C# 7.0 的新语法。 这也是在 2017/03/07 发表的 Visual Studio 2017 中众多新功能之一。

在 C# 7.0 新增了许多支持的语法,重点摆在改善效能、精简程序代码、以及数据取用几个部分。 其中最主要的功能之一是 Tuples, 能让你更容易的一次传回多笔结果,另外 Pattern Match 新语法则简化了撰写针对特定数据型态与条件的程序代码。 除此之外,C# 7.0 也包含了其他重要的各种新语法支持。 希望所有的这些改变都能让你更愉快的写出有效率,简洁的程序代码,同时也更有生产力。

如果你很好奇我们如何导引出这些功能的设计过程,可以查阅 C# Language design GitHub 网站,在那边可以找到设计说明文件,设计提案,与大量的讨论内容。

如果你觉得这篇文章内容很熟悉,也许是你曾经看过去年八月份 (2016/08) 发表过的版本。 在 C# 7.0 最终定案的版本中有少数的异动,这些
异动都来自先前版本的众多优良的回馈意见。

希望你喜欢 C# 7.0, 尽情享受它, Happy Hacking !!

Mads Torgersen, C# Language PM

译注:
为了更清楚的表达这篇文章的内容,翻译时我采用意译,而非逐句翻译。 我也会适时补充字句,让文章要表达的意义更清楚完整。
太多专有名词,翻成中文反而对阅读没有帮助,因此这部分我保留原文,但是我会在译注的部分额外补充说明。
期望这样能更清楚的让读者了解内容。
本篇文章,带您看到以下 C# 7.0 新功能:
  • Out 变量 ( out variables )
  • Pattern Matching (模式匹配 )
  • 使用 pattern 的 is 表达式
  • 使用 patterns 的 switch 语句
  • Tuples
  • Desconstruction (解构 )
  • Local functions ( 区域函式 )
  • 改良的 Literal
  • Ref returns 与 ref locals
  • 异步的传回型别
  • 更广泛的 expression bodies 成员
  • Throw 表达式

Out 变量 (out variables)

在先前版本的 C# 中,out 参数的使用并不如我们期望的那么的流畅。 呼叫带有 out 参数的 method 之前,你必须先宣告变量

并且将它当作 out 的参数传递才行。 通常你不会 (也不需要) 先初始化这变量 (变量的内容会在被呼叫的 method 内覆写),同时你也不能使用 var 的方式来宣告它, 你必须很明确的指定这变量的完整型别:

  1. public void PrintCoordinates(Point p)
  2. {
  3. int x, y; // have to “predeclare”
  4.     p.GetCoordinates(out x, out y);
  5.     WriteLine($“({x}, {y})”);
  6. }

在 C# 7.0,新增了 out 变量,可以在传递 out 参数时同时宣告这个变量:

  1. public void PrintCoordinates(Point p)
  2. {
  3.     p.GetCoordinates(out int x, out int y);
  4.     WriteLine($“({x}, {y})”);
  5. }

请留意,这个变量在包含它本身的 { } 封闭区块范围内,所以接续宣告后面的程序代码可以直接使用这些变量。
多数类似型态的语法没有指定可视范围,该变量可视范围就等同于宣告他的区块范围。

通常 out 变量都会直接被宣告为传递的参数,编译程序通常能直接判定参数的型别为何 (除非 method 包含数个互相冲突
的 overloads 而无法判定),因此可以直接使用 var 的方式来宣告它:

  1. p.GetCoordinates(out var x, out var y);

一般来说,我们常常在 Try… 这类的使用模式中用到 out 参数,它会传回 true 或是 false 来代表执行成功与否,同时借着 out 参数来传回成功执行后的结果:

  1. public void PrintStars(string s)
  2. {
  3.     if (int.TryParse(s, out var i)) { WriteLine(new string(‘*’, i)); }
  4.     else { WriteLine(“Cloudy – no stars tonight!”); }
  5. }

如果你不在意某个 out 参数的传回结果,可以使用 _ 代表忽略它:

  1. p.GetCoordinates(out var x, out _); // I only care about x

Pattern Matching (模式匹配)

C# 7.0 开始引入了 patterns (模式) 的概念。 抽象的来说,他是可以判定数据是否具备一定 “形状”(Shape) 的语法元素,并从该数值之中提取需要的信息。

译注:

Shape, 代表数据的 “形状”, 精确的来说包含数据本身型别包含哪些成员? 这些成员的数值是否落在预期的范围?

patterns 可以让判断数据 “形状” 的程序代码更为简洁明确。

举例来说,C# 7.0 支持的 patterns 有这几种:

  • Constant Patterns (常数模式, 以 c 表示,c 是 C# 的常数表达式), 测试输入的数值是否与 c 相等。
  • Type Patterns (类型模式, 以 T x 表示,T 代表型别,而 x 是识别名称), 测试输入的数值是否属于类别 T? 如果是的话就把输入的数值放到类型为 T 的变量 x 中。
  • Var Patterns (变量模式, 以 var x 表示, x 是识别名称), 这种模式下永远会匹配成功,此时 x 的型别与输入的数值相同,这模式下只是简单的把输入的数值放到 x 之中。

这些只是计划中的第一步 – pattern (模式) 是 C# 新型态的语法元素,我们期望未来能继续新增更多的功能。

在 C# 7.0 我们用 pattern 来增强两种既有的语法结构:

  • is expression (is 表达式) 现在可以在右方加上 pattern,在之前则只能定义型别。
  • switch 语句中的 case 子句,现在可以比对模式是否符合,过去则只支持常数数值。

在未来的 C# 我们会增加更多适用 pattern 的语法。

使用 pattern 的 is 表达式

来看看使用 constant patterns 与 type patterns 的 is expression 使用范例:

  1. public void PrintStars(object o)
  2. {
  3.     if (o is nullreturn;     // constant pattern “null”
  4.     if (!(o is int i)) return// type pattern “int i”
  5.     WriteLine(new string(‘*’, i));
  6. }

如所见,pattern 变量 – 由 pattern 引入的变量,跟前面介绍的 out 变量非常相似,你可以宣告在表达式之中,而且可以直接就近在同可是范围内直接使用他。

跟 out 变量很相似的地方是,模式变量是可变动的,我们常将 out 变量与 pattern 变量,统称为 expression 变量。

Patterns 常与 Try… method 一起使用:

  1. if (o is int i || (o is string s && int.TryParse(s, out i)) { /* use i */ }

使用 patterns 的 switch 语句

在 C# 7.0,我们也扩大了 switch 语句的应用范围:

  • switch 语句现在可以运用在所有型别 (不再只限于基本类型)
  • patterns 可以用在 case 子句
  • case 子句可以附加条件判断式

这边有对应的范例程序代码:

  1. switch(shape)
  2. {
  3.     case Circle c:
  4.         WriteLine($“circle with radius {c.Radius}”);
  5.         break;
  6.     case Rectangle s when (s.Length == s.Height):
  7.         WriteLine($“{s.Length} x {s.Height} square”);
  8.         break;
  9.     case Rectangle r:
  10.         WriteLine($“{r.Length} x {r.Height} rectangle”);
  11.         break;
  12.     default:
  13.         WriteLine(“<unknown shape>”);
  14.         break;
  15.     case null:
  16.         throw new ArgumentNullException(nameof(shape));
  17. }

这里有几个 switch 语句新增的扩充功能:

  • case 子句的顺序是重要的:
    就如同 catch 子句一样,多个 case 子句之间不再是没有顺序关联的,而第一个符合条件的 case 子句会被选中。 这点非常重要,拿上一个范例程序代码来说,代表正方形的这个 case 子句 (译注: case Rectangle s when (s.Length == s.Height):) 应该要排在代表长方形的 case 子句 (case Rectangle r:) 前面,结果才会正确。 另外,就像 catch 子句一样,编译程序可以标示出永远无法执行到的程序代码来协助你。 在这之前,你无法也不需要指定多个 case 之间的评估顺序,所以这并不是个破坏性的改变 (breaking change)。
  • default 子句永远会最后才评估:
    即使在上述的例子中,null case 子句被摆在最后,他仍然会在 default 子句之前被检查。 这样的设计是为了与现有的 switch 陈述句保持兼容。 然而,好的做法通常会明确的将 default 子句摆在最后面。
  • 摆在最后面的 null case 子句并不会无法被被执行到:
    因为 type patterns (类型模式) 依循 is expression 的例子,不会与 null 子句匹配。 这可以确保 null 子句不会不小心被任何的 type patterns (类型模式) 给抢走,你必须更清楚该如何处理这种状况 (或是把它留给 default 子句来处理)。

由 case … 引进的 pattern 变量 ,他的可视范围只限于对应的 switch 区段。

Tuples

想要从一个 method 传回一个以上的传回值是蛮常见的状况。 但是目前 C# 版本对这需求能提供的作法都不够好。 现有的作法有:

  • out 参数:

使用上很累赘 (即使在前面的部分已经介绍了改良的语法),而且这方式也无法搭配 async method 一起使用。

  • 使用 System.Tuple<…> 型别来传回值:
    需要手动配置一个 tuple 对象,同时也需要写些冗长的 code 才能办到。
  • 替每个 method 都自定义专属的传回值型别:
    得额外写大量的 code 来完成这件事,但是目的只是暂时将多个数值组合起来而已。
  • 使用 dynamic 来传回匿名的型别 (anonymous types):
    无法做到静态型别检查,同时将会付出很高的效能代价。

为了让这件事做得更好,C# 7.0 新增了 tuple types 及 tuple literals 的语法:

  1. (stringstringstring) LookupName(long id) // tuple return type
  2. {
  3.     … // retrieve first, middle and last from data storage
  4.     return (first, middle, last); // tuple literal
  5. }

这 method 现在能更有效率的传回三个字符串型别的传回值了,这范例将三个字符串包成一个 tuple。

呼叫这 method 的程序代码将会收到回传的 tuple 对象,且能透过 tuple 对象个别存取这些封装在内的数据:

var names = LookupName(id);
WriteLine($"found {names.Item1} {names.Item3}.");

其中Item1等等,为tuple内的元素预设的名称,这方法能正常运作,但是这命名方式终究不能很能清楚表达用途。所以你愿意的话可以明确的替它们指定更适合的名称:

(string first, string middle, string last) LookupName(long id) // tuple elements have names

现在这个tuple的元素能用更贴切的名称来存取之内的元素了:

var names = LookupName(id);
WriteLine($"found {names.first} {names.last}.");

你也可以直接在tuple literals内指定元素的名称:

    return (first: first, middle: middle, last: last); // named tuple elements in a literal

一般来说,你可以互相指派tuple的变数,而不用管他的名称为何:只要个别的元素都可以被指派,tuple型别可以自由转换为其他的tuple型别。

Tuplesvalue types ,而且它包含的元素都很单纯的被标示为public,都是可异动的栏位( mutable fields )。它们是“数值相等” ( value equality )的,
意思是只要两个tuples的所有对应的元素都是相等的(而且hash code也必须相同),那这两个tuples就是相等的(hash code也会相同) 。

除了传回多个传回值的情况之外,在其他地方tuples也很有用。例如,如果你需要一个包含多个Key的Dictionary,你只需要拿tuple当作Dictionary的Key就可以了。如果你需要在List内的一个元素放置多个不同的数值,只要使用tuple型别并且搜寻这个List。在这些情况中,tuple都能正常运作。

Tuples的实作必须依靠底层的泛型结构型别( generic struct types ): ValueTuple<...>。如果你使用的target framework版本还未包含它,你只需要透过NuGet取得他们即可:

  • 在“方案总管” 内的“专案” 上按右键,选择“管理NuGet 套件…”
  • 选择“浏览” 页签,同时在“套件来源” 项目中选择“nuget.org”
  • 搜寻“System.ValueTuple” 并安装

Desconstruction (解构 )

另一个使用tuples的方式是将他们deconstruct (解构)。Deconstructing declaration (解构宣告)是用来将tuple (或是其他值)里面的部分拆解并个别指派到其他新的变数用的语法:

(string first, string middle, string last) = LookupName(id1); // deconstructing declaration
WriteLine($"found {first} {last}.");

deconstructing declaration (解构宣告)中,可以在个别的变数上使用var:

(var first, var middle, var last) = LookupName(id1); // var inside

甚至你可以在括号外面只用单一一个var:

var (first, middle, last) = LookupName(id1); // var outside

你也可以透过deconstructing assignment (解构指派)将tuple解构后指派到一个既有的变数:

(first, middle, last) = LookupName(id2); // deconstructing assignment

Deconstruction不只适用于tuple,任何型别只要它包含deconstructor (解构式,无论是定义
在instance method或是extension method都可以) ,就可以被解构:

public void Deconstruct(out T1 x1, ..., out Tn xn) { ... }

在这个deconstructor里定义的所有out参数,就是该型别物件解构后的所有项目。
(为何在这边我们使用out参数,而不直接传回tuple ?因为这样就可以让你为不同数量的
变数,分别定义多个overloads (多载))

class Point
{
    public int X { get; }
    public int Y { get; }

    public Point(int x, int y) { X = x; Y = y; }
    public void Deconstruct(out int x, out int y) { x = X; y = Y; }
}

(var myX, var myY) = GetPoint(); // calls Deconstruct(out myX, out myY);

你可以用这样常见的模式,让constructordeconstructor的参数对称排列。
就如同out变数的语法,我们允许你在deconstructor中“忽略”你不在意的out参数:

(var myX, _) = GetPoint(); // I only care about myX

译注:请勿将这里介绍的deconstructor与一般物件导向语言(如: C++, C#都有)常见的descructor搞混了。
这个段落介绍的C#解构式(deconstructor),是定义物件如何“拆解”为多个独立的变数。拆解后原物件仍然存在。
而C#与constructor (建构式)作用相反的descructor (解构函式),则是定义物件要被销毁前必须执行的动作。
两者的中文译名都同样是“解构”请特别留意
对于C# descructor的说明,可以参考: https://msdn.microsoft.com/en-us/library/66x5fx1b.aspx

Local functions (区域函式)

有时,辅助函式只有在使用他的函式内才有意义。现在这种情况下,你可以在其他函式内宣告local functions (区域函式):

public int Fibonacci(int x)
{
    if (x < 0) throw new ArgumentException("Less negativity please!", nameof(x));
    return Fib(x).current;

    (int current, int previous) Fib(int i)
    {
        if (i == 0) return (1, 0);
        var (p, pp) = Fib(i - 1);
        return (p + pp, p);
    }
}

local function (区域函式)内,可以直接使用封闭区块内的parameters (参数)与local variables (区域变数),用法及规则就跟lambda运算式的用法一样。

举例来说,iterator method通常外面都需要包覆另一个non-iterator method,用来在呼叫时做参数检查( iteraotr在这时并不会执行,而是在MoveNext()被呼叫时才会启动)。这时local function就非常适合在这里使用:

public IEnumerable<T> Filter<T>(IEnumerable<T> source, Func<T, bool> filter)
{
    if (source == null) throw new ArgumentNullException(nameof(source));
    if (filter == null) throw new ArgumentNullException(nameof(filter));

    return Iterator();

    IEnumerable<T> Iterator()
    {
        foreach (var element in source) 
        {
            if (filter(element)) { yield return element; }
        }
    }
}

同样的例子,不用local function的话,就必须把该method定义在Filter后面,将iterator宣告为private method。这样会导致封装性被破坏:其他成员可能意外的使用它(而且参数检查会被略过)。同时,所有原本local function需要取用的区域变数与参数,都必须做一样的处理(变成private members)

改良的Literal

C# 7.0允许在number literal (数字常数)中,用_当作digit separator (数字分隔器):

var d = 123_456;
var x = 0xAB_CD_EF;

你可以将_放在数字中的任何位置,来提高程式码的可读性,完全不会对数值本身有任何影响。

此外,C# 7.0也引入二进位的常数表示方式,你现在可以直接用二进位的方式来取代过去十六进位(例: 0x001234)的表示方式。例如:

var b = 0b1010_1011_1100_1101_1110_1111;

Ref returns 与ref locals

如同你可以在C#用参考的方式传递参数(使用ref修饰词),你现在也可以用同样的方式将区域变数的数值用参考的方式传回。

public ref int Find(int number, int[] numbers)
{
    for (int i = 0; i < numbers.Length; i++)
    {
        if (numbers[i] == number) 
        {
            return ref numbers[i]; // return the storage location, not the value
        }
    }
    throw new IndexOutOfRangeException($"{nameof(number)} not found");
}

int[] array = { 1, 15, -39, 0, 7, 14, -12 };
ref int place = ref Find(7, array); // aliases 7's place in the array
place = 9; // replaces 7 with 9 in the array
WriteLine(array[4]); // prints 9

这在回传大型资料结构时相当有用。举例来说,游戏程式可能会预先配置庞大的阵列来存放结构资料(这样是为了避免执行过程中发生garbage collect ,
导致游戏暂停)。现在method可以直接用参考的方式传回结构的资料,呼叫端可以直接读取与修改它的内容。

同时,有些搭配限制来确保这样做是安全的:

  • 你只能传回“能够安全传回”的参考:一个是外界传递给你的参考,另一个是指向目前物件的fields (栏位)的参考。
  • ref locals在初始化时会被指向某个储存位置,一旦指派之后无法再更改。

非同步的传回型别

到目前为止,C#的非同步method限定必须传回void, Task或是Task<T>这几种型别。C# 7.0开始,也允许你用同样的方式,从非同步方法传回你定义的其他型别。

举例来说,我们现在可以定义ValueTask<T>这个struct型别当作传回值。
这可以避免当非同步执行的结果已经可用,但是却因为要进行Task<T>的配置,而导致非同步执行的结果还在等待中( awaiting状态)。许多涉及buffering (缓冲)的非同步操作时,这做法可以明显地降低配置的次数,同时能带来明显的效能提升。

译注:例如非同步I/O的操作,我们会用非同步的方式将档案的内容读到buffer内,完成后再不断重复同样动作,直到档案读取完毕为止,这个动作也许会被重复上千万次。此时由Task<T>替换为ValueTask<T>可能可以带来明显的效能提升。

也有很多其他的情况下,你可以想像自订“task-like”的应用类型会很有用。要正确地建立它们并不是那么的直观,所以我们也不期待大部分的人能正确的使用它们。但是它们可能开始会出现在其他的框架或是API,而呼叫者可以像过去使用Task一样的使用他,传回值与await等待结果

更广泛的expression bodies 成员

在C# 6.0以前,expression bodied methods , properties (属性)等功能大受欢迎,但不是所有的成员都可以
使用。在C# 7.0中,accessors (存取子), constructor (建构式)与finalizers (终结器)都已加到可以使用expression bodies的清单中:

class Person
{
    private static ConcurrentDictionary<int, string> names = new ConcurrentDictionary<int, string>();
    private int id = GetId();

    public Person(string name) => names.TryAdd(id, name); // constructors
    ~Person() => names.TryRemove(id, out *);              // destructors
    public string Name
    {
        get => names[id];                                 // getters
        set => names[id] = value;                         // setters
    }
}

这个新语法的范例程式并非来自Microsoft C# 编译器的团队,而是由社群成员贡献的。太棒了! Open source!

Throw 运算式

要在运算式之中丢出一个例外(exception)是很容易的,只要呼叫method (在method内掷出exception)就可以了。但是在C# 7.0我们允许在运算式之中直接就地丢出exception:

class Person
{
    public string Name { get; }
    public Person(string name) => Name = name ?? throw new ArgumentNullException(nameof(name));
    public string GetFirstName()
    {
        var parts = Name.Split(" ");
        return (parts.Length > 0) ? parts[0] : throw new InvalidOperationException("No name!");
    }
    public string GetLastName() => throw new NotImplementedException();
}

点赞 (0)or分享 (0)
发表我的评论
取消评论
表情 贴图

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
(1)个小伙伴在吐槽
  1. 牛逼呀,都开始玩VS了!这是要玩儿开发的节奏呀!
    明月登楼2017-05-22 08:31 回复 Android 6.0.1 | Chrome 37.0.0.0