• 欢迎访问我爱CSharp学习网,这里有最新最全的C#书籍,C#视频。
  • 如果您觉得本站非常有看点,那么赶紧使用Ctrl+D 收藏我爱C#学习网吧
  • 推荐使用最新版Chrome浏览器和火狐浏览器访问本网站

C# 7.0 新功能介绍

C#杂烩 52csharp 677次浏览 0个评论 扫描二维码

在 C# 7.0 新增了许多支持的语法,重点摆在改善效能、精简程序代码、以及数据取用几个部分。

其中最主要的功能之一是 Tuples, 能让你更容易的一次传回多笔结果,另外 Pattern Match 新语法则简化了撰写针对特定数据型态与条件的程序代码。

除此之外,C# 7.0 也包含了其他重要的各种新语法支持。希望所有的这些改变都能让你更愉快的写出有效率,简洁的程序代码,同时也更有生产力。

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

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

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


1

Out 变数 (out variables)

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

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

public void PrintCoordinates(Point p) {    int x, y; // have to "predeclare"     p.GetCoordinates(out x, out y);    WriteLine($"({x}, {y})"); }


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


public void PrintCoordinates(Point p) {     p.GetCoordinates(out int x, out int y);    WriteLine($"({x}, {y})"); }


请留意,这个变量在包含它本身的 { } 封闭区块范围内,所以接续宣告后面的程序代码可以直接使用这些变量。


多数类似型态的语法没有指定可视范围,该变量可视范围就等同于宣告他的区块范围。
通常 out 变量都会直接被宣告为传递的参数,编译程序通常能直接判定参数的型别为何 (除非 method 包含数个互相冲突的 overloads 而无法判定),因此可以直接使用 var 的方式来宣告它:

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


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


public void PrintStars(string s) {    if (int.TryParse(s, out var i)) { WriteLine(new string('*', i)); }    else { WriteLine("Cloudy - no stars tonight!"); } }

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

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



2

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 的语法。

3

使用 pattern 的 is 表达式

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


public void PrintStars(object o) {    if (o is null) return;     // constant pattern "null"    if (!(o is int i)) return; // type pattern "int i"    WriteLine(new string('*', i)); }


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

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

Patterns 常与 Try… method 一起使用:


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

3

使用 patterns 的 switch 语句

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

  • switch 语句现在可以运用在所有型别 (不再只限于基本类型)

  • patterns 可以用在 case 子句

  • case 子句可以附加条件判断式


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


switch(shape) {    case Circle c:        WriteLine($"circle with radius {c.Radius}");        break;    case Rectangle s when (s.Length == s.Height):        WriteLine($"{s.Length} x {s.Height} square");        break;    case Rectangle r:        WriteLine($"{r.Length} x {r.Height} rectangle");        break;    default:        WriteLine("<unknown shape>");        break;    case null:        throw new ArgumentNullException(nameof(shape)); }


这里有几个 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 区段。


4

Tuples

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

  • out 参数:

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


  • 使用 System.Tuple<…> 型别来传回值:

    需要手动配置一个 tuple 对象,同时也需要写些冗长的 code 才能办到。


  • 替每个 method 都自定义专属的传回值型别:

    得额外写大量的 code 来完成这件事,但是目的只是暂时将多个数值组合起来而已。


  • 使用 dynamic 来传回匿名的型别 (anonymous types):

    无法做到静态型别检查,同时将会付出很高的效能代价。


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


(string, string, string) LookupName(long id) // tuple return type {     ... // retrieve first, middle and last from data storage    return (first, middle, last); // tuple literal }

这 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}.");


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


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


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


Tuples 是 value 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” 并安装

5

Desconstruction (解构 )

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


(string first, string middle, string last) = LookupName(id1); // deconstructing declarationWriteLine($"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);

你可以用这样常见的模式,让 constructor 与 deconstructor 的参数对称排列。

就如同 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



6

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)。

7

改良的 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;

8

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 arrayWriteLine(array[4]); // prints 9


这在回传大型数据结构时相当有用。举例来说,游戏程序可能会预先配置庞大的数组来存放结构数据 (这样是为了避免执行过程中发生 garbage collect,
导致游戏暂停)。

现在 method 可以直接用参考的方式传回结构的数据,呼叫端可以直接读取与修改它的内容。


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

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


9

异步的传回型别

到目前为止,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 等待结果。

10

更广泛的 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!!!

11

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(); }


“阅读原文”给大家推送了 C# Language design GitHub 


我爱CSharp学习网 , 版权所有丨如未注明 , 均为原创丨本网站采用BY-NC-SA协议进行授权 , 转载请注明C# 7.0 新功能介绍
喜欢 (0)
发表我的评论
取消评论

表情 贴图 加粗 删除线 居中 斜体 签到

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

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址