自C#7.0以来,模式匹配就做为C#的一项重要的新特性在不断地演化,这个借鉴于其小弟F#的函数式编程的概念,使得C#的本领愈来愈多,C#9.0就对模式匹配这一功能作了进一步的加强。算法
为了更为深刻和全面的了解模式匹配,在介绍C#9.0对模式匹配加强部分以前,我对模式匹配总体作一个回顾。编程
在特定的上下文中,模式匹配是用于检查所给对象及属性是否知足所需模式(便是否符合必定标准)并从输入中提取信息的行为。它是一种新的代码流程控方式,它能使代码流可读性更强。这里说到的标准有“是否是指定类型的实例”、“是否是为空”、“是否与给定值相等”、“实例的属性的值是否在指定范围内”等。多线程
模式匹配常结合is表达式用在if语句中,也可用在switch语句在switch表达式中,而且能够用when语句来给模式指定附加的过滤条件。它很是善于用来探测复杂对象,例如:外部Api返回的对象在不一样状况下返回的类型不一致,如何肯定对象类型?闭包
从C#的7.0版本到如今9.0版本,总共有以下十三种模式:app
后面内容,咱们就以上这些模式如下面几个类型为基础进行写示例进行说明。ide
public readonly struct Point { public Point(int x, int y) => (X, Y) = (x, y); public int X { get; } public int Y { get; } public void Deconstruct(out int x, out int y) => (x, y) = (X, Y); } public abstract record Shape():IName { public string Name =>this.GetType().Name; } public record Circle(int Radius) : Shape,ICenter { public Point Center { get; init; } } public record Square(int Side) : Shape; public record Rectangle(int Length, int Height) : Shape; public record Triangle(int Base, int Height) : Shape { public void Deconstruct(out int @base, out int height) => (@base, height) = (Base, Height); } interface IName { string Name { get; } } interface ICenter { Point Center { get; init; } }
常量模式是用来检查输入表达式的结果是否与指定的常量相等,这就像C#6.0以前switch语句支持的常量模式同样,自C#7.0开始,也支持is语句。函数式编程
expr is constant
这里expr是输入表达式,constant是字面常量、枚举常量或者const定义常量变量这三者之一。若是expr和constant都是整型类型,那么实质上是用expr == constant来决定二者是否相等;不然,表达式的值经过静态函数Object.Equals(expr, constant)来决定。函数
var circle = new Circle(4); if (circle.Radius is 0) { Console.WriteLine("This is a dot not a circle."); } else { Console.WriteLine($"This is a circle which radius is {circle.Radius}."); }
null模式是个特殊的常量模式,它用于检查一个对象是否为空。ui
expr is null
这里,若是输入表达式expr是引用类型时,expr is null表达式使用(object)expr == null来决定其结果;若是是可空值类型时,使用Nullable
Shape shape = null; if (shape is null) { Console.WriteLine("shape does not have a value"); } else { Console.WriteLine($"shape is {shape}"); }
类型模式用于检测一个输入表达式可否转换成指定的类型,若是能,把转换好的值存放在指定类型定义的变量里。 在is表达式中形式以下:
expr is type variable
其中expr表示输入表达式,type是类型或类型参数名字,variable是类型type定义的新本地变量。若是expr不为空,经过引用、装箱或者拆箱能转化为type或者知足下面任何一个条件,则整个表达式返回值为true,而且expr的转换结果被赋给变量variable。
若是expr是true而且is表达式被用在if语句中,那么variable本地变量仅在if语句内被分配空间进行赋值,本地变量的做用域是从is表达式到封闭包含if语句的块的结束位置。
须要注意的是:声明本地变量的时候,type不能是可空值类型。
Shape shape = new Square(5); if (shape is Circle circle) { Console.WriteLine($"This shape is a {circle.Name} with radius equal to {circle.Radius}"); } else { Console.WriteLine(circle.Radius);//错误,使用了未赋值的本地变量 circle = new Circle(6); Console.WriteLine($"A new {circle.Name} with radius equal to {circle.Radius} is created now."); } //circle变量还处于其做用域内,除非到了封闭if语句的代码块结束的位置。 if (circle is not null && circle.Radius is 0) { Console.WriteLine("This is a dot not a circle."); } else { Console.WriteLine($"This is a circle which radius is {circle.Radius}."); }
上面的包含类型模式的if语句块部分:
if (shape is Circle circle) { Console.WriteLine($"This shape is a {circle.Name} with radius equal to {circle.Radius}"); }
与下面代码是等效的。
var circle = shape as Circle; if (circle != null) { Console.WriteLine($"This shape is a {circle.Name} with radius equal to {circle.Radius}"); }
从上面能够看出,应用类型模式匹配,使得程序代码更为紧凑简洁。
属性模式使你能访问对象实例的属性或者字段来检查输入表达式是否知足指定标准。与is表达式结合使用的基本形式以下:
expr is type {prop1:value1,prop2:value2,...} variable
该模式先检查expr的运行时类型是否能转化成类型type,若是不能,这个模式表达式返回false;若是能,则开始检查其中属性或字段的值匹配,若是有一个不相符,整个匹配结果就为false;若是都匹配,则将expr的对象实例赋给定义的类型为type的本地变量variable。
其中,
下面例子用于检查shape是不是为高和宽相等的长方形,若是是,将其值赋给用Rectangle定义的本地变量rect中:
if (shape is Rectangle { Length: var l,Height:var w } rect && l == w) { Console.WriteLine($"This is a square"); }
属性模式是能够嵌套的,以下检查圆心坐标是否在原点位置,而且半径为100:
if (shape is Circle {Radius:100, Center: {X:0,Y:0} c }) { Console.WriteLine("This is a circle which center is at (0,0)"); }
上面示例与下面代码是等效的,可是采用模式匹配方式写的条件代码量更少,特别是有更多属性须要进行条件检查时,代码量节省更明显;并且上面代码仍是原子操做,不像下面代码要对条件进行4次检查:
if (shape is Circle circle && circle.Radius == 100 && circle.Center.X == 0 && circle.Center.Y == 0) { Console.WriteLine("This is a circle which center is at (0,0)"); }
将类型模式表达形式的type改成var关键字,就成了var模式的表达形式。var模式无论什么状况下,甚至是expr计算机结果为null,它都是返回true。其最大的做用就是捕获expr表达式的值,就是expr表达式的值会被赋给var后的局部变量名。局部变量的类型就是表达式的静态类型,这个变量能够在匹配的模式外部被访问使用。var模式没有null检查,所以在你使用局部变量以前必须手工对其进行null检查。
if (shape is var sh && sh is not null) { Console.WriteLine($"This shape's name is {sh.Name}."); }
将var模式和属性模式相结合,捕获属性的值。示例以下所示。
if (shape is Square { Side: var side } && side > 0 && side < 100) { Console.WriteLine($"This is a square which side is {side} and between 0 and 100."); }
弃元模式是任何表达式均可以匹配的模式。弃元不能看成常量或者类型直接用于is表达式,它通常用于元组、switch语句或表达式。例子参见2.7和4.3相关的例子。
var isShape = shape is _; //错误
元组模式将多个值表示为一个元组,用来解决一些算法有多个输入组合这种状况。以下面的例子结合switch表达式,根据命令和参数值来建立指定图形:
Shape Create(int cmd, int value1, int value2) => (cmd,value1,value2) switch { (0,var v,_)=>new Circle(v), (1,var v,_)=>new Square(v), (2,var l,var h)=>new Rectangle(l,h), (3,var b,var h)=>new Triangle(b,h), (_,_,_)=>throw new NotSupportedException() };
下面是将元组模式用于is表达式的例子。
(Shape shape1, Shape shape2) shapeTuple = (new Circle(100),new Square(50)); if (shapeTuple is (Circle circle, _)) { Console.WriteLine($"This shape is a {circle.Name} with radius equal to {circle.Radius}"); }
位置模式是指经过添加解构函数将类型对象的属性解构成以元组方式组织的离散型变量,以便你可使用这些属性做为一个模式进行检查。
例如咱们给Point结构中添加解构函数Deconstruct,代码以下:
public readonly struct Point { public Point(int x, int y) => (X, Y) = (x, y); public int X { get; } public int Y { get; } public void Deconstruct(out int x, out int y) => (x, y) = (X, Y); }
这样,我就能够将Point结构成不一样的变量。
var point = new Point(10,20); var (x, y) = point; Console.WriteLine($"x = {x}, y = {y}");
解构函数使对象具备了位置模式的功能,使用的时候,看起来像元组模式。例如我用在is语句中例子以下:
if (point is (10,_)) { Console.WriteLine($"This point is (10,{point.Y})"); }
因为位置型record类型,默认已经带有解构函数Deconstruct,所以能够直接使用位置模式。若是是class和struct类型,则须要本身添加解构函数Deconstruct。咱们也能够用扩展方法给一些类型添加解构函数Deconstruct。
关系模式用于检查输入是否知足与常量进行比较的关系约束。形式如: op constant
其中
int? num1 = null; const int low = 0; if (num1 is >low) { }
关系模式与逻辑模式进行结合,功能就会更增强大,帮助咱们处理更多的问题。
int? num1 = null; const int low = 0; double num2 = double.PositiveInfinity; if (num1 is >low and <int.MaxValue && num2 is <double.PositiveInfinity) { }
逻辑模式用于处理多个模式间逻辑关系,就像逻辑运算符!、&&和||同样,优先级顺序也是类似的。为了不与表达式逻辑操做符引发混淆,模式操做符采用单词来表示。他们分别为not、and和or。逻辑模式为多个基本模式进行组合提供了更多可能。
否认模式相似于!操做符,用来检查与指定的模式不匹配的状况。它的关键字是not。例如null模式的否认模式就是检查输入表达式不为null.
if (shape is not null) { // 当shape不为null时的代码逻辑 Console.WriteLine($"shape is {shape}."); }
上面这段代码咱们将否认模式与null模式组合了起来,实现了与下面代码等效的功能,可是易读性更好。
if (!(shape is null)) { // 当shape不为null时的代码逻辑 Console.WriteLine($"shape is {shape}."); }
咱们能够将否认模式与类型模式、属性模式、常量模式等结合使用,用于更多的场景。例以下面例子就将类型模式、属性模式、否认模式和常量模式四种组合起来检查一个图形是不是一个半径不为零的圆。
if (shape is Circle { Radius: not 0 }) { Console.WriteLine("shape is not a dot but a Circle"); }
下面示例判断一个shape若是不是Circle时执行一段逻辑。
if (shape is not Circle circle) { Console.WriteLine("shape is not a Circle"); }
注意:上面这段代码,若是if判断条件为true的话,那么circle的值为null,不能在if语句块中使用,但为false时,circle不为null,即便在if语句块中获得了使用,但也得不到执行,只能在if语句后面使用。
相似于逻辑操做符&&,合取模式就是用and关键词链接两个模式,要求他们都同时匹配。
之前,咱们检查一个对象是不是边长位于(0,100)之间的正方形时,会有以下代码:
if (shape is Square) { var square = shape as Square; if (square.Side > 0 && square.Side < 100) { Console.WriteLine($"This shape is a square with a side {square.Side}"); } }
如今,咱们能够用模式匹配将上述逻辑描述为:
if (shape is Square { Side: > 0 and < 100 } square) { Console.WriteLine($"This shape is a square with a side {square.Side}"); }
这里,咱们将一个类型模式、一个属性模式、一个合取模式、两个关系模式和两个常量模式进行组合。两段一样效果的代码,明显模式匹配代码量更少,没了square.Side的重复出现,更为简洁易懂。
注意事项:
shape is Square and Circle // 编译错误 shape is Square and IName // Ok shape is IName and ICenter // OK
shape is Circle { Radius: 0 and 10 } // 编译错误
shape is Triangle { Base: 10 and Height: 20 } // 编译错误 shape is Triangle { Base: 10 , Height: 20} // OK,是上一句要实现的效果
相似于逻辑操做符||,析取模式就是用or关键词链接两个模式,要求两个模式中有一个能匹配就算匹配成功。
例以下面代码用来检查一个图形是不是边长小于20或者大于60的有效的正方形:
if (shape is Square { Side: >0 and < 20 or > 60 } square) { Console.WriteLine($"This shape is a square with a side {square.Side}"); }
这里,咱们组合运用了类型模式、属性模式、合取模式、析取模式、关系模式和常量模式这六个模式来完成条件判断。看起来很简洁,这个若是用C#9.0以前的代码实现以下,繁琐不少,而且square.Side有重复出现:
if (shape is Square) { var square = shape as Square; if (square.Side > 0 && square.Side < 20 || square.Side>60) { Console.WriteLine($"This shape is a square with a side {square.Side}"); } }
注意事项:
shape is Square or Circle // OK shape is Square or Circle smt // 编译错误,不支持捕捉
shape is Square { Side: 0 or 1 } sq // OK
shape is Rectangle { Height: 0 or Length: 0 } // 编译错误 shape is Rectangle { Height: 0 } or Rectangle { Length: 0 } // OK,实现了上一句想实现的目标
有了以上各类模式及其组合后,就牵扯到一个模式执行优先级顺序的问题,括号模式就是用来改变模式优先级顺序的,这与咱们表达式中括号的使用是同样的。
if (shape is Square { Side: >0 and (< 20 or > 60) } square) { Console.WriteLine($"This shape is a square with a side {square.Side}"); }
有了模式匹配,对因而否为null的判断检查,就显得丰富多了。下面这些均可以用于判断不为null的代码:
if (shape != null)... if (!(shape is null))... if (shape is not null)... if (shape is {})... if (shape is {} s)... if (shape is object)... if (shape is object s)... if (shape is Shape s)...
说到模式匹配,就不得不提与其紧密关联的switch语句、switch表达式和when关键字。
when关键字是在上下文中用来进一步指定过滤条件。只有当过滤条件为真时,后面语句才得以执行。
被用到的上下文环境有:
这里,咱们重点介绍后面二者状况,有关在catch中的应用,若有不清楚的能够查阅相关资料。
在switch语句的when的使用语法以下:
case (expr) when (condition):
这里,expr是常量或者类型模式,condition是when的过滤条件,能够是任何的布尔表达式。具体示例见后面switch语句中的例子。
在switch表达式中when的应用与switch相似,只不过case和冒号被用=>替代而已。具体示例见switch语句表达式。
自C#7.0以后,switch语句被改造且功能更为强大。变化有:
下面方法用于计算指定图形的面积。
static int ComputeArea(Shape shape) { switch (shape) { case null: throw new ArgumentNullException(nameof(shape)); case Square { Side: 0 }: case Circle { Radius: 0 }: case Rectangle rect when rect is { Length: 0 } or { Height: 0 }: case Triangle { Base: 0 } or Triangle { Height: 0 }: return 0; case Square { Side:var side}: return side * side; case Circle c: return (int)(c.Radius * c.Radius * Math.PI); case Rectangle { Length:var l,Height:var h}: return l * h; case Triangle (var b,var h): return b * h / 2; default: throw new ArgumentException("shape is not a recognized shape",nameof(shape)); } }
上面该方法仅用于展现模式匹配多种不一样可能的用法,其中计算面积为0的那一部分实际上是没有必要的。
switch表达式是为在一个表达式的上下文中能够支持像switch语句那样的功能而添加的表达式。
咱们将4.1中的switch语句改成表达式,以下所示:
static int ComputeArea(Shape shape) => shape switch { null=> throw new ArgumentNullException(nameof(shape)), Square { Side: 0 } => 0, Rectangle rect when rect is { Length: 0 } or { Height: 0 } => 0, Triangle { Base: 0 } or Triangle { Height: 0 } => 0, Square { Side: var side } => side*side, Circle c => (int)(c.Radius * c.Radius * Math.PI), Rectangle { Length: var l, Height: var h } => l * h, Triangle (var b, var h) => b * h / 2, _=> throw new ArgumentException("shape is not a recognized shape",nameof(shape)) };
由上例子能够看出,switch表达式与switch语句有如下不一样:
switch表达式的每一个分支=>标记后面的表达式们的最佳公共类型若是存在,而且每一个分支的表达式均可以隐式转换为这个类型,那么这个类型就是switch表达式的类型。
在运行状况下,switch表达式的结果是输入参数第一个匹配到的模式的分支中表达式的值。若是没有匹配到的状况,就会抛出SwitchExpressionException异常。
switch表达式的各个分支状况要全面覆盖输入参数的各类值的状况,不然会报错。这也是弃元在switch表达式中用于表明不可知状况的缘由。
若是switch表达式中一些前面分支老是获得匹配,不能到达后面的分支话,就会出错。这就是弃元模式要放在最后分支的缘由。
从前面不少例子能够看出,模式匹配的不少功能均可以用传统方法实现,那么为何还要用模式匹配呢?
首先,就是咱们前面提到的模式匹配代码量少,简洁易懂,减小代码重复。
再者,就是模式常量表达式在运算时是原子的,只有匹配或者不匹配两种相斥的状况。而多个链接起来的条件比较运算,要屡次进行不一样的比较检查。这样,模式匹配就避免了在多线程场景中的一些问题。
总的来讲,若是可能的话,请使用模式匹配,这才是最佳实践。
这里咱们回顾了全部的模式匹配,也介绍了模式匹配在switch语句和switch表达式中的使用状况,最后介绍了为何使用模式匹配的缘由。