仓颉-模式匹配
模式概述
对于包含匹配值的 match 表达式,case 之后支持哪些模式决定了 match 表达式的表达能力。本节中将依次介绍仓颉支持的模式,包括:常量模式、通配符模式、绑定模式、tuple 模式、类型模式和 enum 模式。
1 常量模式
常量模式可以是整数字面量、浮点数字面量、字符字面量、布尔字面量、字符串字面量(不支持字符串插值)、Unit 字面量。
在包含匹配值的 match 表达式(参见match 表达式)中使用常量模式时,要求常量模式表示的值的类型与待匹配值的类型相同,匹配成功的条件是待匹配的值与常量模式表示的值相等。
下面的例子中,根据 score 的值(假设 score 只能取 0 到 100 间被 10 整除的值),输出考试成绩的等级:
1 | main() { |
编译执行上述代码,输出结果为:A
-
在模式匹配的目标是静态类型为
Rune的值时,Rune字面量和单字符字符串字面量都可用于表示Rune类型字面量的常量 pattern。1
2
3
4
5
6
7
8
9
10
11
12func translate(n: Rune) {
match (n) {
case "A" => 1
case "B" => 2
case "C" => 3
case _ => -1
}
}
main() {
println(translate(r"C"))
}编译执行上述代码,输出结果为:3
-
在模式匹配的目标是静态类型为
Byte的值时,一个表示 ASCII 字符的字符串字面量可用于表示Byte类型字面量的常量 pattern。1
2
3
4
5
6
7
8
9
10
11
12func translate(n: Byte) {
match (n) {
case "1" => 1
case "2" => 2
case "3" => 3
case _ => -1
}
}
main() {
println(translate(51)) // UInt32(r'3') == 51
}编译执行上述代码,输出结果为:3
2 通配符模式
通配符模式使用下划线 _ 表示,可以匹配任意值。通配符模式通常作为最后一个 case 中的模式,用来匹配其他 case 未覆盖到的情况,如常量模式中匹配 score 值的示例中,最后一个 case 中使用 _ 来匹配无效的 score 值。
3 绑定模式
绑定模式使用 id 表示,id 是一个合法的标识符。与通配符模式相比,绑定模式同样可以匹配任意值,但绑定模式会将匹配到的值与 id 进行绑定,在 => 之后可以通过 id 访问其绑定的值。
下面的例子中,最后一个 case 中使用了绑定模式,用于绑定非 0 值:
1 | main() { |
编译执行上述代码,输出结果为:
1 | x is not zero and x = -10 |
使用 | 连接多个模式时不能使用绑定模式,也不可嵌套出现在其他模式中,否则会报错:
1 | main() { |
绑定模式 id 相当于新定义了一个名为 id 的不可变变量(其作用域从引入处开始到该 case 结尾处),因此在 => 之后无法对 id 进行修改。例如,下例中最后一个 case 中对 n 的修改是不允许的。
1 | main() { |
对于每个 case 分支,=> 之后变量作用域级别与 case 后 => 前引入的变量作用域级别相同,在 => 之后再次引入相同名字会触发重定义错误。例如:
1 | main() { |
注意:
当模式的 identifier 为 enum 构造器时,该模式会被当成 enum 模式进行匹配,而不是绑定模式(关于 enum 模式,详见 enum 模式章节)。
1 | enum RGBColor { |
编译执行上述代码,输出结果为:
1 | red |
4 Tuple 模式
Tuple 模式用于 tuple 值的匹配,它的定义和 tuple 字面量类似:(p_1, p_2, ..., p_n),区别在于这里的 p_1 到 p_n(n 大于等于 2)是模式(可以是本章节中介绍的任何模式,多个模式间使用逗号分隔)而不是表达式。
例如,(1, 2, 3) 是一个包含三个常量模式的 tuple 模式,(x, y, _) 是一个包含两个绑定模式,一个通配符模式的 tuple 模式。
给定一个 tuple 值 tv 和一个 tuple 模式 tp,当且仅当 tv 每个位置处的值均能与 tp 中对应位置处的模式相匹配,才称 tp 能匹配 tv。例如,(1, 2, 3) 仅可以匹配 tuple 值 (1, 2, 3),(x, y, _) 可以匹配任何三元 tuple 值。
下面的例子中,展示了 tuple 模式的使用:
1 | main() { |
编译执行上述代码,输出结果为:
1 | Alice is 24 years old |
同一个 tuple 模式中不允许引入多个名称相同的绑定模式。例如,下例中最后一个 case 中的 case (x, x) 是不合法的。
1 | main() { |
5 类型模式
类型模式用于判断一个值的运行时类型是否是某个类型的子类型。类型模式有两种形式:_: Type(嵌套一个通配符模式 _)和 id: Type(嵌套一个绑定模式 id),它们的区别是后者会发生变量绑定,而前者不会。
对于待匹配值 v 和类型模式 id: Type(或 _: Type),首先判断 v 的运行时类型是否是 Type 的子类型,若成立则视为匹配成功,否则视为匹配失败;如匹配成功,则将 v 的类型转换为 Type 并与 id 进行绑定(对于 _: Type,不存在绑定这一操作)。
假设有如下两个类,Base 和 Derived,并且 Derived 是 Base 的子类,Base 的无参构造函数中将 a 的值设置为 10,Derived 的无参构造函数中将 a 的值设置为 20:
1 | open class Base { |
下面的代码展示了使用类型模式并匹配成功的例子:
1 | main() { |
编译执行上述代码,输出结果为:
1 | r = 20 |
下面的代码展示了使用类型模式但类型模式匹配失败的例子:
1 | open class Base { |
编译执行上述代码,输出结果为:
1 | r = 0 |
6 enum 模式
enum 模式用于匹配 enum 类型的实例,它的定义和 enum 的构造器类似:无参构造器 C 或有参构造器 C(p_1, p_2, ..., p_n),构造器的类型前缀可以省略,区别在于这里的 p_1 到 p_n(n 大于等于 1)是模式。例如,Some(1) 是一个包含一个常量模式的 enum 模式,Some(x) 是一个包含一个绑定模式的 enum 模式。
给定一个 enum 实例 ev 和一个 enum 模式 ep,当且仅当 ev 的构造器名字和 ep 的构造器名字相同,且 ev 参数列表中每个位置处的值均能与 ep 中对应位置处的模式相匹配,才称 ep 能匹配 ev。例如,Some("one") 仅可以匹配 Option<String> 类型的Some 构造器 Option<String>.Some("one"),Some(x) 可以匹配任何 Option 类型的 Some 构造器。
下面的例子中,展示了 enum 模式的使用,因为 x 的构造器是 Year,所以会和第一个 case 匹配:
1 | enum TimeUnit { |
编译执行上述代码,输出结果为:
1 | x has 24 months |
使用 | 连接多个 enum 模式:
1 | enum TimeUnit { |
使用 match 表达式匹配 enum 值时,要求 case 之后的模式要覆盖待匹配 enum 类型中的所有构造器,如果未做到完全覆盖,编译器将报错:
1 | enum RGBColor { |
可以通过加上 case Blue 来实现完全覆盖,也可以在 match 表达式的最后通过使用 case _ 来覆盖其他 case 未覆盖的到的情况,如:
1 | enum RGBColor { |
上述代码的执行结果为:
1 | Other |
模式的嵌套组合
Tuple 模式和 enum 模式可以嵌套任意模式。下面的代码展示了不同模式嵌套组合使用:
1 | enum TimeUnit { |
编译并执行上述代码,输出结果为:
1 | Set year 2022 |
模式的 Refutability
模式可以分为两类:可反驳 refutable 模式和不可反驳 irrefutable 模式。在类型匹配的前提下,当一个模式有可能和待匹配值不匹配时,称此模式为 refutable 模式;反之,当一个模式总是可以和待匹配值匹配时,称此模式为 irrefutable 模式。
对于上述介绍的各种模式,规定如下:
常量模式是 refutable 模式。例如,下例中第一个 case 中的 1 和第二个 case 中的 2 都有可能和 x 的值不相等。
1 | func constPat(x: Int64) { |
通配符模式是 irrefutable 模式。例如,下例中无论 x 的值是多少,_ 总能和其匹配。
1 | func wildcardPat(x: Int64) { |
绑定模式是 irrefutable 模式。例如,下例中无论 x 的值是多少,绑定模式 a 总能和其匹配。
1 | func varPat(x: Int64) { |
Tuple 模式是 irrefutable 模式,当且仅当其包含的每个模式都是 irrefutable 模式。例如,下例中 (1, 2) 和 (a, 2) 都有可能和 x 的值不匹配,所以它们是 refutable 模式,而 (a, b) 可以匹配任何 x 的值,所以它是 irrefutable 模式。
1 | func tuplePat(x: (Int64, Int64)) { |
类型模式是 refutable 模式。例如,下例中(假设 Base 是 Derived 的父类,并且 Base 实现了接口 I),x 的运行时类型有可能既不是 Base 也不是 Derived,所以 a: Derived 和 b: Base 均是 refutable 模式。
1 | interface I {} |
enum 模式是 irrefutable 模式,当且仅当它对应的 enum 类型中只有一个有参构造器,且 enum 模式中包含的其他模式也是 irrefutable 模式。例如,对于下例中的 E1 和 E2 定义,函数 enumPat1 中的 A(1) 是 refutable 模式,A(a) 是 irrefutable 模式;而函数 enumPat2 中的 B(b) 和 C(c) 均是 refutable 模式。
1 | enum E1 { |
match 表达式
match 表达式的定义
仓颉支持两种 match 表达式,第一种是包含待匹配值的 match 表达式,第二种是不含待匹配值的 match 表达式。
含匹配值的 match 表达式:
1 | main() { |
match 表达式以关键字 match 开头,后跟要匹配的值(如上例中的 x,x 可以是任意表达式),接着是定义在一对花括号内的若干 case 分支。
每个 case 分支以关键字 case 开头,case 之后是一个模式或多个由 | 连接的相同种类的模式(如上例中的 1、0、_ 都是模式,详见模式概述章节);模式之后可以接一个可选的 pattern guard,表示本条 case 匹配成功后额外需要满足的条件,pattern guard 使用 where cond 表示,要求表达式 cond 的类型为 Bool;接着是一个 =>,=> 之后即本条 case 分支匹配成功后需要执行的操作,可以是一系列表达式、变量和函数定义(新定义的变量或函数的作用域从其定义处开始到下一个 case 之前结束),如上例中的变量定义和 print 函数调用。
match 表达式执行时依次将 match 之后的表达式与每个 case 中的模式进行匹配,一旦匹配成功(如果有 pattern guard,也需要 where 之后的表达式的值为 true;如果 case 中有多个由 | 连接的模式,只要待匹配值和其中一个模式匹配则认为匹配成功)则执行 => 之后的代码然后退出 match 表达式的执行(意味着不会再去匹配它之后的 case),如果匹配不成功则继续与它之后的 case 中的模式进行匹配,直到匹配成功(match 表达式可以保证一定存在匹配的 case 分支)。
上例中,因为 x 的值等于 0,所以会和第二条 case 分支匹配(此处使用的是常量模式,匹配的是值是否相等,详见常量模式章节),最后输出 x = 0。
编译并执行上述代码,输出结果为:
1 | x = 0 |
match 表达式要求所有匹配必须是穷尽(exhaustive)的,意味着待匹配表达式的所有可能取值都应该被考虑到。当 match 表达式非穷尽,或者编译器判断不出是否穷尽时,均会编译报错,换言之,所有 case 分支(包含 pattern guard)所覆盖的取值范围的并集,应该包含待匹配表达式的所有可能取值。常用的确保 match 表达式穷尽的方式是在最后一个 case 分支中使用通配符模式 _,因为 _ 可以匹配任何值。
match 表达式的穷尽性保证了一定存在和待匹配值相匹配的 case 分支。下面的例子将编译报错,因为所有的 case 并没有覆盖 x 的所有可能取值:
1 | func nonExhaustive(x: Int64) { |
如果被匹配值的类型包含 enum 类型且该 enum 为 non-exhaustive enum,则其在匹配时需要使用可匹配所有构造器的模式,如通配符模式 _ 和绑定模式。
1 | enum T { |
在 case 分支的模式之后,可以使用 pattern guard 进一步对匹配出来的结果进行判断。
在下面的例子中(使用到了 enum 模式,详见 enum 模式章节),当 RGBColor 的构造器的参数值大于等于 0 时,输出它们的值,当参数值小于 0 时,认为它们的值等于 0:
1 | enum RGBColor { |
编译执行上述代码,输出结果为:
1 | Green = 0 |
没有匹配值的 match 表达式:
1 | main() { |
与包含待匹配值的 match 表达式相比,关键字 match 之后并没有待匹配的表达式,并且 case 之后不再是 pattern,而是类型为 Bool 的表达式(上述代码中的 x > 0 和 x < 0)或者 _(表示 true),当然,case 中也不再有 pattern guard。
无匹配值的 match 表达式执行时依次判断 case 之后的表达式的值,直到遇到值为 true 的 case 分支;一旦某个 case 之后的表达式值等于 true,则执行此 case 中 => 之后的代码,然后退出 match 表达式的执行(意味着不会再去判断该 case 之后的其他 case)。
上例中,因为 x 的值等于 -1,所以第二条 case 分支中的表达式(即 x < 0)的值等于 true,执行 print("x < 0")。
编译并执行上述代码,输出结果为:
1 | x < 0 |
match 表达式的类型
对于 match 表达式(无论是否有匹配值):
- 在上下文有明确的类型要求时,要求每个
case分支中=>之后的代码块的类型是上下文所要求的类型的子类型; - 在上下文没有明确的类型要求时,
match表达式的类型是每个case分支中=>之后的代码块的类型的最小公共父类型; - 当
match表达式的值没有被使用时,其类型为Unit,不要求各分支的类型有最小公共父类型。
下面分别举例说明。
1 | let x = 2 |
上面的例子中,定义变量 s 时,显式地标注了其类型为 String,属于上下文类型信息明确的情况,因此要求每个 case 的 => 之后的代码块的类型均是 String 的子类型,显然上例中 => 之后的字符串类型的字面量均满足要求。
再来看一个没有上下文类型信息的例子:
1 | let x = 2 |
上例中,定义变量 s 时,未显式标注其类型,因为每个 case 的 => 之后的代码块的类型均是 String,所以 match 表达式的类型是 String,进而可确定 s 的类型也是 String。
其他使用模式的地方
模式除了可以在 match 表达式中使用外,还可以使用在变量定义(等号左侧是一个模式)和 for in 表达式(for 关键字和 in 关键字之间是一个模式)中。
但是,并不是所有的模式都能使用在变量定义和 for in 表达式中,只有 irrefutable 的模式才能在这两处被使用,所以只有通配符模式、绑定模式、irrefutable tuple 模式和 irrefutable enum 模式是允许的。
-
变量定义和
for in表达式中使用通配符模式的例子如下:1
2
3
4
5
6main() {
let _ = 100
for (_ in 1..5) {
println("0")
}
}上例中,变量定义时使用了通配符模式,表示定义了一个没有名字的变量(当然此后也就没办法对其进行访问),
for in表达式中使用了通配符模式,表示不会将1..5中的元素与某个变量绑定(当然循环体中就无法访问1..5中元素值)。编译执行上述代码,输出结果为:1
2
3
40
0
0
0 -
变量定义和
for in表达式中使用绑定模式的例子如下:1
2
3
4
5
6
7main() {
let x = 100
println("x = ${x}")
for (i in 1..5) {
println(i)
}
}上例中,变量定义中的
x以及for in表达式中的i都是绑定模式。编译执行上述代码,输出结果为:1
2
3
4
5x = 100
1
2
3
4 -
变量定义和
for in表达式中使用irrefutabletuple 模式的例子如下:1
2
3
4
5
6
7
8main() {
let (x, y) = (100, 200)
println("x = ${x}")
println("y = ${y}")
for ((i, j) in [(1, 2), (3, 4), (5, 6)]) {
println("Sum = ${i + j}")
}
}上例中,变量定义时使用了 tuple 模式,表示对
(100, 200)进行解构并分别和x与y进行绑定,效果上相当于定义了两个变量x和y。for in表达式中使用了 tuple 模式,表示依次将[(1, 2), (3, 4), (5, 6)]中的 tuple 类型的元素取出,然后解构并分别和i与j进行绑定,循环体中输出i + j的值。编译执行上述代码,输出结果为:1
2
3
4
5x = 100
y = 200
Sum = 3
Sum = 7
Sum = 11 -
变量定义和
for in表达式中使用irrefutableenum 模式的例子如下:1
2
3
4
5
6
7
8
9
10enum RedColor {
Red(Int64)
}
main() {
let Red(red) = Red(0)
println("red = ${red}")
for (Red(r) in [Red(10), Red(20), Red(30)]) {
println("r = ${r}")
}
}上例中,变量定义时使用了 enum 模式,表示对
Red(0)进行解构并将构造器的参数值(即0)与red进行绑定。for in表达式中使用了 enum 模式,表示依次将[Red(10), Red(20), Red(30)]中的元素取出,然后解构并将构造器的参数值与r进行绑定,循环体中输出r的值。编译执行上述代码,输出结果为:1
2
3
4red = 0
r = 10
r = 20
r = 30