C#模式匹配大全
声明模式
1、语法
声明模式用于简单判断一个模糊的数据类型是否是某个具体的数据类型,并尝试将其转换过去。
1 | public class Person |
这段代码等价于下面这样的代码:
1 | object o = new Person{Name="hello",Age=18} |
即在大括号里等效进行类型转换。
2、声明模式仍可能会进行拆箱
假设我们原始的对象是装箱的操作:
1 | object o = 3; |
那么,即使你使用这个语法来获取结果:
1 | if (o is int v) |
它也避免不了拆箱行为:因为它等于 o is int 后直接进行 int v = (int)o
的拆箱赋值操作,所以它会隐式地进行拆箱,它是避免不了的。
Var模式
假如对上面的Person进行判定,name必须以大写字母开头
1 | static bool IsValid(Person p) |
上面要多声明一个name临时变量
其实可以用下面var模式进行代替
1 | static bool IsValid2(Person p) |
firstChar其实就是声明了一个临时变量,其实var也可以使用对应的类型关键字来代替,这样就和声明模式完全一样了
1 | static bool IsValid3(Person p) |
也就是说var 模式和声明模式的书写格式完全一样,唯一的区别是,一个写的是类型的具体名称,一个则是写的固定的关键字 var。声明模式下,写的数据具体类型会作为数据的判断类型进行判断;而 var 仅等价于变量声明,它并不具有任何的数据类型的判断。
常量模式
1 | object o = 3; |
等价于
1 | object o = 3; |
- 特殊情况是double.NaN(not a number),
var s = double.NaN;
,a == double.NaN
吗?double 类型的等号比较是严格的,因此很多时候比较起来都不一定相等。因此,C# 的 API 有一个 double.IsNaN 方法在专门对这个情况进行判断
1 | var s = double.NaN; |
输出结果
对位模式
我们经常遇到这种比较方式
1 | var p = new Person{Name="hello",Age=10}; |
解构函数
可以使用解构函数来实现上面的比较方式
在类中声明Deconstruct
函数,其返回值为void,且名字固定
1 | public class Person |
此时可以进行解构
1 | var (name, age) = p; |
上面的比较可以使用对位模式
1 | if (p is ("hello",10)) |
解构函数可以重载,但是相同参数个数,不同参数类型的情况是不被允许的
主构造器的对位模式
C# 9 和 C# 10 分别诞生了记录类型和记录结构类型,它们必须绑定一个主构造器位于声明的头部:
record Person(string Name, int Age, bool IsBoy);
在使用模式匹配的时候,由于编译器会自动生成对应的解构函数,因此我们可以直接对主构造器使用对位模式匹配。
if (person is Person(Name: "Sunnie", Age: 25, true)) ;
扩展方法对位模式
当引用外部库,我们无法修改内部代码时,可以使用扩展方法对位模式
1 | public class Person |
解构模式
因为前文我们拥有了解构函数,也拥有了 var 模式,因此 C# 灵活的语法提供了 var 模式的解构版本:
if (point is var (x, y) && x == 30 && y == 30)
稍微注意一下这里的语法是写成 var (x, y)。当然,也可以内联 var 关键字。和值元组的语法一致,依然可以用 (var x, var y)
的语法。
if (point is (var x, var y) && x == 30 && y == 30)
这样是可以的。
var (x, y) 是解构模式,而 (var x, var y) 是对位模式。因为前者使用 var (x, y) 语法,小括号里直接定义了变量名,小括号的外侧则是 var 关键字;
但 (var x, var y) 在小括号里定义了两个变量,都使用了 var 关键字,这意味着是对应位置上的数据分别定义变量,类似point.X is var x && point.Y is var y
的效果,因此只能说是对位模式。
使用解构模式可以更清楚、更简明地将对象进行解构,直接赋值到变量上;但它存在一定的弊端,例如解构模式下就不能往里判断数值了。也就是说,你在写成 var (a, b) 的类似语法后,就无法往 a、b 上使用任何模式匹配的判别语法了。
可空类型的解构
1 | Point? nullable = new Point; |
这就不单纯和 var 模式一样。它牵扯到数据是不是 null 才可解构的问题。如果数据都是 null 了,我们就无法解构。因此,可空值类型的解构模式会先判断对象是不是不为 null,然后才是解构。
if (nullable != null && nullable.Value is (x: var x, y: var y))
nullable != null 和 nullable.HasValue 是等效的,所以写 nullable.HasValue 也没问题。
弃元模式
假设前文的解构函数存在的话,那么我们必然会解构成两个数据(x 和 y)。但是,如果我们仅判断 x 的数据,而不关心 y 是多少的话,我们可以使用一个下划线 _ 来表示“y 我们不用判断”,或者说“y 的模式匹配总是成立的”。
if (nullable is (x: 30, y: _))
属性模式
使用{}
来进行匹配,可以将{}里面的东西看做是筛选条件
1.定义一个类
1 | public class Person |
- 普通用法
1 | var p = new Person { Name = "hello", Age = 10 }; |
- 弃元
1 | var p = new Person { Name = "hello", Age = 10 }; |
- 匹配不为null,也就是对其属性等不做任何显示,只要非空即可
1 | var p = new Person { Name = "hello", Age = 10 }; |
但是,对于可空值类型T?,匹配的是T的属性,而不是T?的属性,
1 | int? i = null; |
- 属性模式递归
1 | if (p is { Name: "hello", Friend: {Age:10} }) |
属性模式与其他模式组合
语法:表达式 is 类型模式 (对位模式) {属性模式} 声明模式
1 | //递归模式可以和对位模式、类型模式、声明模式放在一起 |
关系模式
有些时候,数据判断和取值无法对一个范围来判断,因此还不够灵活。C# 里还有关系模式,来对数据的范围来判断。
if (obj is > 30)
即使 obj 不是 int 类型,我们依旧可以这么写。这个代码等价于 obj is int i && i > 30
C# 允许 >、>=、< 和 <= 四个运算符,写在 is 后,来表达范围判断。稍微注意一下的地方是,is > 30 的 30 必须是常量才行
1 | var p = new Person { Name = "hello", Age = 10 }; |
逻辑模式
因为模式匹配里的每个模式并不是一个“数据信息”,因此我们无法直接对模式用 &&、|| 等符号来进行拼接组合。C# 为了解决这个问题,多了三个关键字:and、or 和 not 来拼接模式。
1、合取模式
合取模式用 and 拼接模式,来表达这些模式都必须成立。
static bool IsLowerLetter(char c) => c is >= 'a' and <= 'z';
比如这里,>= ‘a’ and <= ‘z’ 整个表达式用来表达,>= ‘a’ 和 <= ‘z’ 两个条件必须都满足。如果要写分开,就必须写成 c is >= 'a' && c is <= 'z'
。
2、析取模式
析取模式用 or 拼接。
static bool IsLetter(char c) => c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z');
注意,or 拼接了前面 >= ‘a’ and <= ‘z’ 和后面 >= ‘A’ and <= ‘Z’ 两个模式。or 表示两个模式有一个模式能够匹配成功就可以。
这里我们介绍了一种新的语法:C# 允许模式匹配的内部使用小括号,来断开和分隔一个模式。and 和 or 的模式名称不变,但这个小括号套起来的模式,C# 称之为括号模式。
3、取反模式
取反模式用 not。
if (input is not null)
最常见的就是这里。我们如果判断对象是不是不为 null,那么我们最常用的就是写成 is not null。is null 属于前面的常量模式,判断对象是不是 null。它和 == 运算符的区别是,== 运算符可重载,重载会影响 == 的判断和使用逻辑;而 is 是永远不变的判断模式。
4、混用三种模式
也可以混用到 and 和 or 关键字拼接起来的模式里。
if (ch is >= '0' and <= '9' or '.')
5、三种模式的优先级和结合性
合取式 and 和数学上是一样的,比 or 更优先推理,因此无需对 and 和 or 模式一起的复杂模式匹配添加括号:
static bool IsLetter(char c) => c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z');
比如这样,(>= ‘a’ and <= ‘z’) 和 (>= ‘A’ and <= ‘Z’) 的小括号可以不要。
取反式的话,因为它只和一个模式结合使用,不像是 and 和 or 需要两个模式结合,因此 not 的优先级比 and 和 not 都要高。
6、字面量在 and 模式下的类型可调整性
字面量有时候表现得并不一定非得是字面量本身的数据类型。
举个例子,1 是 int 类型的字面量,但我们可以使用 and 连接常量模式和类型模式,使得这个 1 的类型发生变化:
1 | object o = 1U; |
请注意这里的 uint and 1 模式。uint 是表示类型必须是 uint 类型,而 1 却又是 int 类型的字面量,这不会冲突吗?答案是并不会,字面量在模式匹配里会按照 and 里联立给出的类型进行隐式转换。如果能够转换过去,那么就是允许的。
拓展属性模式
因为属性模式本身有些地方很鸡肋,因为它可以嵌套,比如下面这样的代码:
1 | if ( |
这是之前的属性递归介绍的代码。这个写法里,Father 里再次包含一层大括号。
在新的模式匹配里,为了解决这种大括号嵌套太多层次导致可读性降低的问题,发明了拓展属性模式。拓展属性模式允许当判断某一个属性的时候将这个代码简写为 Father.Name:
1 | if ( |
即少一个大括号的层级级别:Prop: { NestProp: { } }
改成 Prop.NestedProp { }
的格式。
注意:使用Father.Name时,不用关心Father为Null的情况,编译器会自动判断,如果前面的属性值为null,则会停止判断
列表模式
列表模式是将一个不知道是不是集合的对象,用列表的格式列举出来,对其中的元素挨个进行判断的模式。
我们使用一对中括号进行判断。使用范围记号 .. 来表达“这是一个范围”。举个例子:[1, .., 3] 表示判断一个序列的第一个元素是不是 1,而最后一个元素是不是 3。所以,自然这个写法就等价于下面这个格式了:
f (arr is { Length: 10 } and [1, .., 3])
它等价于
if (arr.Length == 10 && arr[0] == 1 && arr[^1] == 3)
这里的 ^1 是 C# 8 里的表达式,表示倒数第一个元素。^n 就是倒数第 n 个元素。可以从这个写法里看出,.. 是灵活的:它不是固定长度,是随着整个模式匹配的序列来确定 .. 的长度的。这么写是为了简化代码的书写格式。
当然,假设我们判断倒数第二个元素而不是倒数第一个的话,那么我们可以尝试在倒数第一个元素的判断信息上添加弃元记号 _ 来表达占位:
if (arr is { Length: 10 } and [1, .., 3, _])
使用条件
- 对象必须含有Count或者Length属性
- 对象至少包含一个int参数的单参数索引器
有两个容易弄错的类型Dictionary<TKey, TValue>和 ICollection<T>
,因为字典的索引器不是int类型,而ICollection<T>
压根没有索引器。
列表模式不是递归模式的一部分
列表模式是一个单独的模式,你必须声明得和别的模式串联起来用 and 或 or 连接,所以如下的语法是错的:
if (o is int[] [1, _, .., 3])
正确的做法是在 int[] 和 [1, _, .., 3] 之间插入一个 and:
if (o is int[] and [1, _, .., 3])
这一点一定要记住,因为它是对集合作判断,但大多数类型也都不是集合,因此不要想着把它和别的模式放在一起;但是,列表模式允许定义内联变量。
if (expr is [_, _, ..] result)
假设此时的 expr 是一个表达式,我们可以使用此语法定义表达结果,并视 result 为表达式的运算结果。
分片(切片)模式
C# 允许对集合类型的分片。举个例子。
if (arr is { Length: 10 } and [_, .. var slice, _])
这段代码表示,我们将中间的 8 个元素提取出来,变成一个列表。它等价于这个写法:
if (arr.Length == 10 && arr[1..^1] is var slice)
一定要注意,分片是前闭后开的半开区间
分片嵌套
分片模式允许我们对范围记号 .. 的内容进行内联变量定义,但这样的代码仍不够灵活。我还想要内联模式匹配的话,C# 是提供了这个机制的。看看这样的代码是什么意思?
if (arr is [_, _, .. [_, .., 7], _])
这个模式要求我们列表集合里至少含有三个以上的元素。接着,.. [_, .., 7]
是分片模式的嵌套模式匹配。我们从第三个元素开始判断,我们必须要求从第三个元素开始,我们包含一个序列,它至少有两个元素,且最后一个元素是 7。
与其他模式组合
如果这个数组的每一个元素并不是简单的类型,那么它里面可能包含一些别的元素。这个时候我们可能会在分片后,使用别的模式进行模式匹配:
if (nestedArr is [_, .. [{ Prop: 42 } sliced, ..], _])
比如这里.. [{ Prop: 42 } sliced, ..]
就是一个典型的嵌套用法。.. 后跟上 [{ Prop: 42 } sliced, ..] 是一个嵌套进去的分片模式。其中,它判断分片后的序列至少包含 1 个元素,且第一个元素必须满足模式 { Prop: 42 } sliced,也就是 Prop 属性必须是 42。如果成功匹配,那么这个元素名称可以使用 sliced 标识符引用。