使用C#8.0中的模式执行更多操作

Visual Studio 2019预览版2已经发布! 有了它,您可以尝试更多的C#8.0功能。 它主要与模式匹配有关,尽管最后我将介绍其他一些新闻和更改。


原始博客

在更多地方有更多图案


当C#7.0引入模式匹配时,我们说过我们希望在将来的更多地方添加更多模式。 那个时候到了! 我们将添加所谓的递归模式 ,以及更紧凑的switch语句的表达式形式,称为(您猜对了!)switch 表达式


这是一个简单的C#7.0模式示例,可以让我们开始:


 class Point { public int X { get; } public int Y { get; } public Point(int x, int y) => (X, Y) = (x, y); public void Deconstruct(out int x, out int y) => (x, y) = (X, Y); } static string Display(object o) { switch (o) { case Point p when pX == 0 && pY == 0: return "origin"; case Point p: return $"({pX}, {pY})"; default: return "unknown"; } } 

切换表达式


首先,让我们观察一下,许多switch语句在case主体中确实没有做很多有趣的工作。 通常,它们都只是通过将值分配给变量或通过返回值来产生值(如上所述)。 在所有这些情况下,坦率地说switch语句都比较笨拙。 感觉就像是拥有5多年历史的语言功能,而且还有很多仪式。


我们认为是时候添加switch的表达形式了。 在这里,适用于以上示例:


 static string Display(object o) { return o switch { Point p when pX == 0 && pY == 0 => "origin", Point p => $"({pX}, {pY})", _ => "unknown" }; } 

这里有几件事与switch语句有所不同。 让我们列出它们:


  • switch关键字是测试值和案例列表之间的“中缀”。 这使得它与其他表达式更加融合,并且更容易从视觉上区分switch语句。
  • 为简洁起见, case关键字和:已替换为lambda箭头=>
  • 为简洁起见, default已被_丢弃模式替换。
  • 身体是表情! 所选正文的结果成为switch表达式的结果。

由于表达式需要具有值或引发异常,因此未匹配而到达末尾的switch表达式将引发异常。 当可能出现这种情况时,编译器会尽力警告您,但不会强迫您以包罗万象的结尾来结束所有switch表达式:您可能会更好!


当然,由于我们的Display方法现在由单个return语句组成,因此我们可以将其简化为表达式形式:


  static string Display(object o) => o switch { Point p when pX == 0 && pY == 0 => "origin", Point p => $"({pX}, {pY})", _ => "unknown" }; 

老实说,我不确定我们将在此处给出什么格式指导,但是应该清楚,这更加简洁明了,尤其是因为简洁起见,您通常可以使用“表格”格式来设置开关格式,如上所述,其中模式和主体在同一行上,并且=>彼此对齐。


顺便说一句,我们计划在最后一种情况之后允许尾随逗号,以与C#中的所有其他“大括号中的逗号分隔列表”保持一致,但是Preview 2尚不允许这样做。


属性模式


简而言之,模式突然变成了上面switch表达式中最重的元素! 让我们为此做些事情。


请注意,switch表达式使用类型模式 Point p (两次)以及when子句为第一种case添加其他条件。


在C#8.0中,我们向类型模式添加了更多可选元素,这允许模式本身进一步挖掘与模式匹配的值。 您可以通过添加{...}的包含嵌套模式的属性模式,以将其应用于值的可访问属性或字段。 让我们如下重写switch表达式:


 static string Display(object o) => o switch { Point { X: 0, Y: 0 } p => "origin", Point { X: var x, Y: var y } p => $"({x}, {y})", _ => "unknown" }; 

两种情况仍然检查o是否为Point 。 然后,第一种情况将常量模式0递归应用于pXY属性,检查它们是否具有该值。 因此when在这种情况和许多常见情况下,我们可以消除when子句。


第二种情况将var模式应用于XY每个。 回想一下,C#7.0中的var模式始终会成功,并且只需声明一个新鲜变量即可保存该值。 因此, xy包含pXpY


我们从不使用p ,实际上可以在这里省略它:


  Point { X: 0, Y: 0 } => "origin", Point { X: var x, Y: var y } => $"({x}, {y})", _ => "unknown" 

对于所有类型模式(包括属性模式)而言,一件事都是正确的,那就是它们要求该值必须为非null。 这就打开了将“空”属性模式{}用作紧凑的“非空”模式的可能性。 例如,我们可以将后备情况替换为以下两种情况:


  {} => o.ToString(), null => "null" 

{}处理剩余的非null对象,而null获得null,因此切换是详尽的,编译器将不会抱怨值丢失。


位置模式


属性模式并不能完全缩短第二个Point大小写,似乎不值得在那儿麻烦,但还有很多事情可以做。


注意Point类具有Deconstruct方法,即所谓的deconstructor 。 在C#7.0中,解构函数允许在赋值时解构一个值,因此您可以编写例如:


 (int x, int y) = GetPoint(); // split up the Point according to its deconstructor 

C#7.0没有将解构与模式集成在一起。 这随位置模式而变化,这是我们在C#8.0中扩展类型模式的另一种方式。 如果匹配类型是元组类型或具有解构函数,则可以使用位置模式作为应用递归模式的紧凑方式,而不必命名属性:


 static string Display(object o) => o switch { Point(0, 0) => "origin", Point(var x, var y) => $"({x}, {y})", _ => "unknown" }; 

一旦将对象匹配为Point ,就应用解构函数,并将嵌套模式应用于结果值。


解构函数并不总是合适的。 仅应将它们添加到真正清楚哪个值是哪个值的类型中。 例如,对于Point类,假定第一个值为X ,第二个值为Y ,则是安全直观的,因此上述开关表达式直观且易于阅读。


元组模式


位置模式的一个非常有用的特殊情况是将其应用于元组。 如果将switch语句直接应用于元组表达式,我们甚至允许省略多余的括号集,如switch (x, y, z)而不是switch ((x, y, z))


元组模式非常适合同时测试多个输入。 这是状态机的简单实现:


 static State ChangeState(State current, Transition transition, bool hasKey) => (current, transition) switch { (Opened, Close) => Closed, (Closed, Open) => Opened, (Closed, Lock) when hasKey => Locked, (Locked, Unlock) when hasKey => Closed, _ => throw new InvalidOperationException($"Invalid transition") }; 

当然,我们可以选择在已打开的元组中包含hasKey而不是使用when子句-这实际上是一个问题:


 static State ChangeState(State current, Transition transition, bool hasKey) => (current, transition, hasKey) switch { (Opened, Close, _) => Closed, (Closed, Open, _) => Opened, (Closed, Lock, true) => Locked, (Locked, Unlock, true) => Closed, _ => throw new InvalidOperationException($"Invalid transition") }; 

总之,我希望您能看到递归模式和switch表达式可以导致更清晰和更具声明性的程序逻辑。


预览2中的其他C#8.0功能


虽然模式功能是VS 2019 Preview 2中上线的主要功能,但是我希望您能从中找到一些有用的功能和有趣的一些小功能。 我不会在这里详细介绍,而只是给您一个简短的描述。


使用声明


在C#中, using语句总是导致一定程度的嵌套,这可能会非常烦人并损害可读性。 对于仅希望在作用域末尾清除资源的简单情况,现在可以使用声明代替。 using声明只是局部变量声明,其前面带有using关键字,它们的内容位于当前语句块的末尾。 所以代替:


 static void Main(string[] args) { using (var options = Parse(args)) { if (options["verbose"]) { WriteLine("Logging..."); } ... } // options disposed here } 

你可以简单地写


 static void Main(string[] args) { using var options = Parse(args); if (options["verbose"]) { WriteLine("Logging..."); } } // options disposed here 

一次性引用结构


引用结构是在C#7.2中引入的,并不是在这里重申其有用性,但是作为回报,它们具有一些严重的局限性,例如无法实现接口。 现在,仅通过在其中具有Dispose方法,就可以在不实现IDisposable接口的情况下使用ref结构。


静态局部功能


如果要确保您的局部函数不会产生与包围范围中的“捕获”(引用)变量相关的运行时开销,则可以将其声明为static 。 然后,编译器将阻止引用封闭函数中声明的任何内容-除了其他静态局部函数!


自预览版1以来的更改


预览1的主要功能是可为空的引用类型和异步流。 两者在Preview 2中都有一些改进,因此,如果您开始使用它们,则以下几点很容易意识到。


可空引用类型


我们在源代码中(通过#nullable#pragma warning指令)和项目级别添加了更多选项来控制可为空的警告。 我们还将项目文件选择加入更改为<NullableContextOptions>enable</NullableContextOptions>


异步流


我们更改了编译器期望的IAsyncEnumerable<T>接口的形状! 这会使编译器与.NET Core 3.0 Preview 1中提供的接口不同步,这可能会引起一些麻烦。 但是,.NET Core 3.0 Preview 2即将发布,这将使界面恢复同步。


加油!


一如既往,我们热切期待您的反馈! 请特别试用新的图案功能。 你碰到砖墙吗? 有烦人的事吗? 您为他们找到了哪些有趣且有用的方案? 点击反馈按钮,让我们知道!


快乐黑客


Mads Torgersen,C#设计负责人

Source: https://habr.com/ru/post/zh-CN438256/


All Articles