撰写了文章 发布于 2020-04-26 20:20:20
从C#开始的编程入门——模式匹配
模式匹配(pattern matching)实际上是一个相对宽泛的概念。简单来说,模式就是一种规则,匹配就是检查一个东西是不是满足这些规则。
首先我们通过经典的形状抽象作为我们的示例用类。在这篇文章的示例中,我们不会尝试通过构造一个基类或者接口的方法来解决这个问题,而是尝试使用模式匹配。
定义都很简单,都是一些非常简单的图形。现在如果我要求实现一个可以求各种图形面积的方法,那么应该如何设计呢?这些结构都不是来自于同一个基类的类型,甚至没有相同的属性。
struct Rectangle
{
public double Height { set; get; }
public double Length { set; get; }
public Rectangle(double height, double length)
{
Height = height;
Length = length;
}
}
struct Square
{
public double Side { set; get; }
public Square(double side) => Side = side;
}
struct Circle
{
public double Radius { set; get; }
public Circle(double radius) => Radius = radius;
}
首先要考虑的问题是如何获得一个形状。这个时候由于我们没有基类,也没有接口,我们只能把它退化成object。
public static double Area(object shape)
自然而然的,不同形状计算面积的方法各有不同,我们使用object来接受各种不同的形状之后,自然需要判断它究竟是哪一种形状。这个时候必然会用到is来判断具体类型,确定类型之后再尝试类型转换继而获得对应类型的成员的访问权限,通过这样的思路可以写出下面的代码:
public static double Area(object shape)
{
if (shape is Square)
{
var square = (Square)shape;
return square.Side * square.Side;
}
if (shape is Circle)
{
var circle = (Circle)shape;
return MathF.PI * circle.Radius * circle.Radius;
}
// ……
}
is运算符在前面我们已经介绍过它的功能之一就是判断某个对象是否是某个类型。接下来我们将进一步了解is的用法。
is与模式匹配
在上面的代码中,我们使用is判断类型,然后声明了一个局部变量然后转换为对应的类型。从C#7.0开始,实际上使用is运算符时可以将上述两个操作合并到一起:
if (shape is Square square)
{
return square.Side * square.Side;
}
也就是说如果shape是Square类型,那么条件为真,随即把shape转换为Square类型的square,然后就可以在后面的代码块中使用。这样的针对类型进行的测试被称为类型模式。
类型模式
类型模式(Type Pattern)会检查表达式是否能够被转换为某种类型,如果可以就执行转换。is的类型模式语法如下:
expr is type varname
当满足下列任意一个条件,并且expr不为null时,is表达式的结果为true:
- expr是type类型的实例
- expr是type类型的派生类的实例
- expr的编译时类型(静态类型)是type的基类,而其运行时类型(动态类型)是type或type的派生类
- expr是一个实现了type接口的类型的实例
常量模式
常量模式(Constant Pattern)比较好理解。它会检查is左边的表达式是否等于右边常量表达式。语法如下:
expr is constant
其中constant可以是下列的常量表达式:
- 字面量
- 使用const关键字定义的常量
- 枚举类型常量
比如:
enum TicTacToe { O, X }
int n = 1;
double d = 3d;
const double PI = 3.14;
TicTacToe tic = TicTacToe.O;
System.Console.WriteLine(n is 1);
System.Console.WriteLine(d is PI);
System.Console.WriteLine(tic is TicTacToe.O);
constant表达式会按照以下规则求值:
- 如果expr和constant都是整数类型,则直接返回==运算符操作的结果
- 否则调用Object.Equals(expr, constant)
除了常量之外,constant还可以是null,也就等于是进行null检查。
var模式
var模式给人的感觉会比较怪。var模式总是会匹配成功。它会创建一个名为varname的临时变量,类型和expr的类型一致。如果expr为null,则变量的值也为null。
expr is var vorname
要注意的是,这样的表达式也无法作为一句单独的语句,因此它不能单独出现。通常它可能会被安排在一连串逻辑表达式中,我们可能会需要对一个较长的表达式进行逻辑运算,我们可以利用这种模式来给它取一个别名:
object o = new Square(1d);
System.Console.WriteLine(o is var shape && (shape is Rectangle || shape is Square));
switch与模式匹配
前面介绍过的switch主要用于判断一个表达式的取值情况,并且一般来说要么是常量要么是一些字面量。具体来说,在C#6.0及以前的版本,switch后面的括号中的表达式(被称为匹配表达式,match expression)支持char、string、bool、整数类型、枚举类型。
而从C#7.0开始,这个表达式可以是任何非null的表达式。而switch中用于匹配的case也支持上面提到的几种模式。对于一个类型不够具体的表达式我们可以通过各种模式来进行对应的操作。
object o = new Square(1d);
switch (o)
{
case null:
System.Console.WriteLine(“Nothing”);
break;
case Square square:
System.Console.WriteLine(“A square”);
break;
default:
System.Console.WriteLine(“Something weird”);
break;
}
case Square square实际上就是一个类型模式,只不过is用case替代了而已很好理解。和使用is相比,利用switch进行的模式匹配可以让多个模式匹配的代码更好地组织在一起而不是使用一连串if-is-else。
when
自C#7.0开始,各个case不再需要是互斥的。互斥是什么意思呢,就是一个事件发生时,其它时间不能同时发生。比如说一个针对int的switch,一个case为1,那么其它case就不允许为1,从而一旦匹配到1,那么也就不可能匹配到其它case。由于对模式匹配的支持,switch允许出现多个使用相同模式的case,但是需要搭配一个when子句来进一步判断。
when子句后面需要一个bool类型的表达式,当某个case匹配成功时,会根据when子句的情况来判断是否要执行该case下的代码。
case Square square when square.Side == 0:
case Circle circle when circle.Radius == 0:
System.Console.WriteLine(“0”);
break;
case Square square:
System.Console.WriteLine(“A square”);
break;
当o为一个正方形时并且边长为0时就会输出0,而不为0时输出A square。
值得注意的是,最终还是只会匹配到唯一的一个case。并且如果带有when子句的case和相同模式不带when子句的模式同时出现,带when子句的case需要出现在前面。举例来说,如果被匹配的表达式确实是一个Square,那么如果首先出现的是不带when的case,那么无论它的边长情况如何,都必然可以匹配,然后退出switch,带when的版本永远都匹配不到。
switch表达式
switch表达式是从C#8.0开始支持的新特性。在此之前的switch都是语句,而现在switch可以是表达式。还记得表达式和语句的区别吗?
表达式总是会返回一个值。之前的switch语句中通常完成匹配后便会执行一些代码然后退出switch语句。而switch表达式同样会针对一个表达式进行模式匹配但是最终会返回一个结果。
switch表达式的语法和switch语句有一些不一样的地方,初见会觉得非常别扭。首先匹配表达式出现在switch关键字的前方(在switch表达式中被称为范围表达式,range expression),紧接着是一个大括号,其中有数个由逗号分隔的表达式分支(switch expression arm),每个表达式分支由模式,=>符号,和一个表达式组成。由于这是一个表达式(尽管它已经超出了一般印象中的表达式的长度),很多时候不要忘记最后的分号(但是又要记得每个分支的表达式最后是逗号不是分号。。)。
利用switch表达式我们甚至可以把之前的Area函数简化成这样的一个表达式:
object shape = new Square(2);
double? area = shape switch
{
null => null,
Square { Side:0d } s => 0,
Circle circle when circle.Radius == 0 => 0,
Square s => s.Side * s.Side,
};
switch表达式同样会帮我们完成模式匹配工作。switch会将范围表达式和内部的分支的模式进行匹配,成功匹配一个模式时,便会返回=>右边的表达式的值。从而把前面Area函数中的类型检查和计算并返回面积一气呵成了。
不过这个超长的表达式还没有说完。第二个分支有一种看似很奇怪的语法。这里要知道的第一点是,和switch语句一样,同一个类型模式可以有多个分支。第二就是这个Square { Side:0d } s实际上是一个递归模式。遇到这个模式时,首先依然会执行类型模式进行类型检查,如果匹配成功,那么就检查大括号里面的属性是否等于对应的值,这里也就是边长是否为0。这个语法和声明对象调用构造器之后立即初始化属性的语法是类似的,只不过这里变成了冒号。
之后的Circle模式中暗示的内容是,switch表达式一样支持when子句。它可以达到和前面的递归模式类似的效果,但是递归模式中只能支持和对象的属性和字段的匹配,而when可以是任何bool表达式。
弃元
前面的switch表达式实际上还有点问题,你如果照着我的代码写了同样的代码,你会得到一个警告。大概就是说你的switch表达式没有处理完所有可能的输入。
确实,我们前面只写了null、Square、Circle三种类型的模式,而shape是一个object,它在运行时可能是任何类型的对象。switch语句中我们使用default来处理我们明确定义的case以外的任何值,那么switch表达式中我们如何处理“任意”模式呢。
不幸的是,我们无法在switch表达式中使用default关键字。但是我们可以使用弃元(discard,本意是丢弃的意思)。
弃元由下划线表示,弃元本身也可以作为一种模式,它可以代表任何模式,反过来说也就是我们不关心这个东西究竟是怎么样的。要在switch表达式中实现default的效果只需要像这样使用弃元模式:
_ => null,
除此之外,在类型模式中如果我们只需要进行类型匹配而不管该对象具体的情况(我们不需要访问它的成员),也可以使用弃元:
Square _ => “It’s a square”,
除了在switch和is中,弃元还可以在其他地方发挥作用。
在元组析构中使用弃元
元组通常包含多个元素,但是我们可能对某些元素并不感兴趣。在析构时我们可以使用弃元直接丢掉某个值。
var person = (name: “John”, age:19);
var (_, age) = person;
name就被丢弃了。
除此之外弃元还可以在一些其它的地方使用,这里主要介绍和模式匹配相关的作用,有关弃元更详细的介绍可以参见这里。
目录