
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
递归应用于p
的X
和Y
属性,检查它们是否具有该值。 因此when
在这种情况和许多常见情况下,我们可以消除when
子句。
第二种情况将var
模式应用于X
和Y
每个。 回想一下,C#7.0中的var
模式始终会成功,并且只需声明一个新鲜变量即可保存该值。 因此, x
和y
包含pX
和pY
。
我们从不使用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();
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..."); } ... }
你可以简单地写
static void Main(string[] args) { using var options = Parse(args); if (options["verbose"]) { WriteLine("Logging..."); } }
一次性引用结构
引用结构是在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#设计负责人