模式匹配
本章主要介绍仓颉中的模式匹配(pattern matching),首先介绍 match
表达式和模式,然后介绍模式的 refutability(即某个模式是否一定能匹配成功),最后介绍模式匹配在 match
表达式之外的使用。
match 表达式
仓颉支持两种 match
表达式,第一种是包含待匹配值的 match
表达式,第二种是不含待匹配值的 match
表达式。
含匹配值的 match 表达式举例:
main() {
let x = 0
match (x) {
case 1 => let r1 = "x = 1"
print(r1)
case 0 => let r2 = "x = 0" // Matched.
print(r2)
case _ => let r3 = "x != 1 and x != 0"
print(r3)
}
}
match
表达式以关键字 match
开头,后跟要匹配的值(如上例中的 x
,x
可以是任意表达式),接着是定义在一对花括号内的若干 case
分支。
每个 case
分支以关键字 case
开头,case
之后是一个模式或多个由 |
连接的相同种类的模式(如上例中的 1
、0
、_
都是模式,详见模式章节);模式之后可以接一个可选的 pattern guard
,表示本条 case
匹配成功后额外需要满足的条件;接着是一个 =>
,=>
之后即本条 case
分支匹配成功后需要执行的操作,可以是一系列表达式、变量和函数定义(新定义的变量或函数的作用域从其定义处开始到下一个 case
之前结束),如上例中的变量定义和 print
函数调用。
match
表达式执行时依次将 match
之后的表达式与每个 case
中的模式进行匹配,一旦匹配成功(如果有 pattern guard
,也需要 where
之后的表达式的值为 true
;如果 case
中有多个由 |
连接的模式,只要待匹配值和其中一个模式匹配则认为匹配成功)则执行 =>
之后的代码然后退出 match
表达式的执行(意味着不会再去匹配它之后的 case
),如果匹配不成功则继续与它之后的 case
中的模式进行匹配,直到匹配成功(match
表达式可以保证一定存在匹配的 case
分支)。
上例中,因为 x
的值等于 0
,所以会和第二条 case
分支匹配(此处使用的是常量模式,匹配的是值是否相等,详见常量模式章节),最后输出 x = 0
。
编译并执行上述代码,输出结果为:
x = 0
match
表达式要求所有匹配必须是穷尽(exhaustive)的,意味着待匹配表达式的所有可能取值都应该被考虑到。当 match
表达式非穷尽,或者编译器判断不出是否穷尽时,均会编译报错,换言之,所有 case
分支(包含 pattern guard)所覆盖的取值范围的并集,应该包含待匹配表达式的所有可能取值。常用的确保 match
表达式穷尽的方式是在最后一个 case
分支中使用通配符模式 _
,因为 _
可以匹配任何值。
match
表达式的穷尽性保证了一定存在和待匹配值相匹配的 case
分支。下面的例子将编译报错,因为所有的 case
并没有覆盖 x
的所有可能取值:
func nonExhaustive(x: Int64) {
match (x) {
case 0 => print("x = 0")
case 1 => print("x = 1")
case 2 => print("x = 2")
}
}
在 case
分支的模式之后,可以使用 pattern guard
进一步对匹配出来的结果进行判断。pattern guard
使用 where cond
表示,要求表达式 cond
的类型为 Bool
。
在下面的例子中(使用到了 enum
模式,详见 Enum 模式章节),当 RGBColor
的构造器的参数值大于等于 0
时,输出它们的值,当参数值小于 0
时,认为它们的值等于 0
:
enum RGBColor {
| Red(Int16) | Green(Int16) | Blue(Int16)
}
main() {
let c = RGBColor.Green(-100)
let cs = match (c) {
case Red(r) where r < 0 => "Red = 0"
case Red(r) => "Red = ${r}"
case Green(g) where g < 0 => "Green = 0" // Matched.
case Green(g) => "Green = ${g}"
case Blue(b) where b < 0 => "Blue = 0"
case Blue(b) => "Blue = ${b}"
}
print(cs)
}
编译执行上述代码,输出结果为:
Green = 0
没有匹配值的 match 表达式举例:
main() {
let x = -1
match {
case x > 0 => print("x > 0")
case x < 0 => print("x < 0") // Matched.
case _ => print("x = 0")
}
}
与包含待匹配值的 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")
。
编译并执行上述代码,输出结果为:
x < 0
match 表达式的类型
对于 match
表达式(无论是否有匹配值),
-
在上下文有明确的类型要求时,要求每个
case
分支中=>
之后的代码块的类型是上下文所要求的类型的子类型; -
在上下文没有明确的类型要求时,
match
表达式的类型是每个case
分支中=>
之后的代码块的类型的最小公共父类型。 -
当
match
表达式的值没有被使用时,其类型为Unit
,不要求各分支的类型有最小公共父类型。
下面分别举例说明。
let x = 2
let s: String = match (x) {
case 0 => "x = 0"
case 1 => "x = 1"
case _ => "x != 0 and x != 1" // Matched.
}
上面的例子中,定义变量 s
时,显式地标注了其类型为 String
,属于上下文类型信息明确的情况,因此要求每个 case
的 =>
之后的代码块的类型均是 String
的子类型,显然上例中 =>
之后的字符串类型的字面量均满足要求。
再来看一个没有上下文类型信息的例子:
let x = 2
let s = match (x) {
case 0 => "x = 0"
case 1 => "x = 1"
case _ => "x != 0 and x != 1" // Matched.
}
上例中,定义变量 s
时,未显式标注其类型,因为每个 case
的 =>
之后的代码块的类型均是 String
,所以 match
表达式的类型是 String
,进而可确定 s
的类型也是 String
。
模式
对于包含匹配值的 match
表达式,case
之后支持哪些模式决定了 match
表达式的表达能力,本节中我们将依次介绍仓颉支持的模式,包括:常量模式、通配符模式、绑定模式、tuple 模式、类型模式和 enum 模式。
常量模式
常量模式可以是整数字面量、浮点数字面量、字符字面量、布尔字面量、字符串字面量(不支持字符串插值)、Unit 字面量。
在包含匹配值的 match
表达式中使用常量模式时,要求常量模式表示的值的类型与待匹配值的类型相同,匹配成功的条件是待匹配的值与常量模式表示的值相等。
下面的例子中,根据 score
的值(假设 score
只能取 0
到 100
间被 10
整除的值),输出考试成绩的等级:
main() {
let score = 90
let level = match (score) {
case 0 | 10 | 20 | 30 | 40 | 50 => "D"
case 60 => "C"
case 70 | 80 => "B"
case 90 | 100 => "A" // Matched.
case _ => "Not a valid score"
}
println(level)
}
编译执行上述代码,输出结果为:
A
通配符模式
通配符模式使用下划线 _
表示,可以匹配任意值。通配符模式通常作为最后一个 case
中的模式,用来匹配其他 case
未覆盖到的情况,如上节中匹配 score
值的示例中,最后一个 case
中使用 _
来匹配无效的 score
值。
绑定模式
绑定模式使用 id
表示,id
是一个合法的标识符。与通配符模式相比,绑定模式同样可以匹配任意值,但绑定模式会将匹配到的值与 id
进行绑定,在 =>
之后可以通过 id
访问其绑定的值。
下面的例子中,最后一个 case
中使用了绑定模式,用于绑定非 0
值:
main() {
let x = -10
let y = match (x) {
case 0 => "zero"
case n => "x is not zero and x = ${n}" // Matched.
}
println(y)
}
编译执行上述代码,输出结果为:
x is not zero and x = -10
使用 |
连接多个模式时不能使用绑定模式,也不可嵌套出现在其它模式中,否则会报错:
main() {
let opt = Some(0)
match (opt) {
case x | x => {} // Error: variable cannot be introduced in patterns connected by '|'
case Some(x) | Some(x) => {} // Error: variable cannot be introduced in patterns connected by '|'
case x: Int64 | x: String => {} // Error: variable cannot be introduced in patterns connected by '|'
}
}
绑定模式 id
相当于新定义了一个名为 id
的不可变变量(其作用域从引入处开始到该 case
结尾处),因此在 =>
之后无法对 id
进行修改。例如,下例中最后一个 case
中对 n
的修改是不允许的。
main() {
let x = -10
let y = match (x) {
case 0 => "zero"
case n => n = n + 0 // Error: 'n' cannot be modified.
"x is not zero"
}
println(y)
}
对于每个 case
分支,=>
之后变量作用域级别与 case
后 =>
前引入的变量作用域级别相同,在 =>
之后再次引入相同名字会触发重定义错。例如:
main() {
let x = -10
let y = match (x) {
case 0 => "zero"
case n => let n = 0 // Error, redefinition
println(n)
"x is not zero"
}
println(y)
}
注:当模式的 identifier 为 enum 构造器时,该模式会被当成 enum 模式进行匹配,而不是绑定模式(关于 enum 模式,详见 Enum 模式章节)。
enum RGBColor {
| Red | Green | Blue
}
main() {
let x = Red
let y = match (x) {
case Red => "red" // The 'Red' is enum mode here.
case _ => "not red"
}
println(y)
}
编译执行上述代码,输出结果为:
red
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 模式的使用:
main() {
let tv = ("Alice", 24)
let s = match (tv) {
case ("Bob", age) => "Bob is ${age} years old"
case ("Alice", age) => "Alice is ${age} years old" // Matched, "Alice" is a constant pattern, and 'age' is a variable pattern.
case (name, 100) => "${name} is 100 years old"
case (_, _) => "someone"
}
println(s)
}
编译执行上述代码,输出结果为:
Alice is 24 years old
同一个 tuple 模式中不允许引入多个名字相同的绑定模式。例如,下例中最后一个 case
中的 case (x, x)
是不合法的。
main() {
let tv = ("Alice", 24)
let s = match (tv) {
case ("Bob", age) => "Bob is ${age} years old"
case ("Alice", age) => "Alice is ${age} years old"
case (name, 100) => "${name} is 100 years old"
case (x, x) => "someone" // Error: Cannot introduce a variable pattern with the same name, which will be a redefinition error.
}
println(s)
}
类型模式
类型模式用于判断一个值的运行时类型是否是某个类型的子类型。类型模式有两种形式:_: Type
(嵌套一个通配符模式 _
)和 id: Type
(嵌套一个绑定模式 id
),它们的差别是后者会发生变量绑定,而前者并不会。
对于待匹配值 v
和类型模式 id: Type
(或 _: Type
),首先判断 v
的运行时类型是否是 Type
的子类型,若成立则视为匹配成功,否则视为匹配失败;如匹配成功,则将 v
的类型转换为 Type
并与 id
进行绑定(对于 _: Type
,不存在绑定这一操作)。
假设有如下两个类,Base
和 Derived
,并且 Derived
是 Base
的子类,Base
的无参构造函数中将 a
的值设置为 10
,Derived
的无参构造函数中将 a
的值设置为 20
:
open class Base {
var a: Int64
public init() {
a = 10
}
}
class Derived <: Base {
public init() {
a = 20
}
}
下面的代码展示了使用类型模式并匹配成功的例子:
main() {
var d = Derived()
var r = match (d) {
case b: Base => b.a // Matched.
case _ => 0
}
println("r = ${r}")
}
编译执行上述代码,输出结果为:
r = 20
下面的代码展示了使用类型模式但类型模式匹配失败的例子:
open class Base {
var a: Int64
public init() {
a = 10
}
}
class Derived <: Base {
public init() {
a = 20
}
}
main() {
var b = Base()
var r = match (b) {
case d: Derived => d.a // Type pattern match failed.
case _ => 0 // Matched.
}
println("r = ${r}")
}
编译执行上述代码,输出结果为:
r = 0
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
匹配:
enum TimeUnit {
| Year(UInt64)
| Month(UInt64)
}
main() {
let x = Year(2)
let s = match (x) {
case Year(n) => "x has ${n * 12} months" // Matched.
case TimeUnit.Month(n) => "x has ${n} months"
}
println(s)
}
编译执行上述代码,输出结果为:
x has 24 months
使用 |
连接多个 enum 模式:
enum TimeUnit {
| Year(UInt64)
| Month(UInt64)
}
main() {
let x = Year(2)
let s = match (x) {
case Year(0) | Year(1) | Month(_) => "ok" // Ok
case Year(2) | Month(m) => "invalid" // Error: Variable cannot be introduced in patterns connected by '|'
case Year(n: UInt64) | Month(n: UInt64) => "invalid" // Error: Variable cannot be introduced in patterns connected by '|'
}
println(s)
}
使用 match
表达式匹配 enum
值时,要求 case
之后的模式要覆盖待匹配 enum
类型中的所有构造器,如果未做到完全覆盖,编译器将报错:
enum RGBColor {
| Red | Green | Blue
}
main() {
let c = Green
let cs = match (c) { // Error: Not all constructors of RGBColor are covered.
case Red => "Red"
case Green => "Green"
}
println(cs)
}
我们可以通过加上 case Blue
来实现完全覆盖,也可以在 match
表达式的最后通过使用 case _
来覆盖其他 case
未覆盖的到的情况,如:
enum RGBColor {
| Red | Green | Blue
}
main() {
let c = Blue
let cs = match (c) {
case Red => "Red"
case Green => "Green"
case _ => "Other" // Matched.
}
println(cs)
}
上述代码的执行结果为:
Other
模式的嵌套组合
Tuple 模式和 enum 模式可以嵌套任意模式。下面的代码展示了不同模式嵌套组合使用:
enum TimeUnit {
| Year(UInt64)
| Month(UInt64)
}
enum Command {
| SetTimeUnit(TimeUnit)
| GetTimeUnit
| Quit
}
main() {
let command = SetTimeUnit(Year(2022))
match (command) {
case SetTimeUnit(Year(year)) => println("Set year ${year}")
case SetTimeUnit(Month(month)) => println("Set month ${month}")
case _ => ()
}
}
编译执行上述代码,输出结果为:
Set year 2022
模式的 Refutability
模式可以分为两类:refutable
模式和 irrefutable
模式。在类型匹配的前提下,当一个模式有可能和待匹配值不匹配时,称此模式为 refutable
模式;反之,当一个模式总是可以和待匹配值匹配时,称此模式为 irrefutable
模式。
对于上述介绍的各种模式,规定如下:
常量模式是 refutable
模式。例如,下例中第一个 case 中的 1
和第二个 case 中的 2
都有可能和 x
的值不相等。
func constPat(x: Int64) {
match (x) {
case 1 => "one"
case 2 => "two"
case _ => "_"
}
}
通配符模式是 irrefutable
模式。例如,下例中无论 x
的值是多少,_
总能和其匹配。
func wildcardPat(x: Int64) {
match (x) {
case _ => "_"
}
}
绑定模式是 irrefutable
模式。例如,下例中无论 x
的值是多少,绑定模式 a
总能和其匹配。
func varPat(x: Int64) {
match (x) {
case a => "x = ${a}"
}
}
Tuple 模式是 irrefutable
模式,当且仅当其包含的每个模式都是 irrefutable
模式。例如,下例中 (1, 2)
和 (a, 2)
都有可能和 x
的值不匹配,所以它们是 refutable
模式,而 (a, b)
可以匹配任何 x
的值,所以它是 irrefutable
模式。
func tuplePat(x: (Int64, Int64)) {
match (x) {
case (1, 2) => "(1, 2)"
case (a, 2) => "(${a}, 2)"
case (a, b) => "(${a}, ${b})"
}
}
类型模式是 refutable
模式。例如,下例中(假设 Base
是 Derived
的父类,并且 Base
实现了接口 I
),x
的运行时类型有可能既不是 Base
也不是 Derived
,所以 a: Derived
和 b: Base
均是 refutable
模式。
interface I {}
open class Base <: I {}
class Derived <: Base {}
func typePat(x: I) {
match (x) {
case a: Derived => "Derived"
case b: Base => "Base"
case _ => "Other"
}
}
Enum 模式是 irrefutable
模式,当且仅当它对应的 enum
类型中只有一个有参构造器,且 enum 模式中包含的其他模式也是 irrefutable
模式。例如,对于下例中的 E1
和 E2
定义,函数 enumPat1
中的 A(1)
是 refutable
模式,A(a)
是 irrefutable
模式;而函数 enumPat2
中的 B(b)
和 C(c)
均是 refutable
模式。
enum E1 {
A(Int64)
}
enum E2 {
B(Int64) | C(Int64)
}
func enumPat1(x: E1) {
match (x) {
case A(1) => "A(1)"
case A(a) => "A(${a})"
}
}
func enumPat2(x: E2) {
match (x) {
case B(b) => "B(${b})"
case C(c) => "C(${c})"
}
}
if-let 与 while-let 表达式
在一些应用场景中,我们只关注一个表达式是否为某种特定模式,是则将其解构,取出对应值做相关操作。虽然可以用 match
表达式实现这一逻辑,但书写上可能比较冗长,为此,仓颉提供了更便捷的表达方式——允许在 if
表达式和 while
表达式的条件部分,使用 let
修饰符匹配和解构模式,这时它们被称为 if-let
表达式和 while-let
表达式。
在条件部分使用 let
匹配模式的形式为:
let 模式 <- 表达式
这里的“模式”可以是常量模式、通配符模式、绑定模式、Tuple
模式和 enum
模式,如果模式中含有占位标识符,则此处等同于定义了一个不可变变量(这正是使用 let
表达此语义的原因),如果模式被匹配,这个变量就会与解构后的值绑定,可以作为 if
分支或 while
循环体中的局部变量使用。
if-let 表达式
if-let
表达式首先对条件中 let
等号右侧的表达式进行求值,如果此值能匹配 let
等号左侧的模式,则执行 if
分支,否则执行 else
分支(可省略)。例如:
main() {
let result = Option<Int64>.Some(2023)
if (let Some(value) <- result) {
println("操作成功,返回值为:${value}")
} else {
println("操作失败")
}
}
运行以上程序,将输出:
操作成功,返回值为:2023
对于以上程序,如果将 result
的初始值修改为 Option<Int64>.None
,则 if-let
的模式匹配会失败,将执行 else
分支:
main() {
let result = Option<Int64>.None
if (let Some(value) <- result) {
println("操作成功,返回值为:${value}")
} else {
println("操作失败")
}
}
运行以上程序,将输出:
操作失败
while-let 表达式
while-let
表达式首先对条件中 let
等号右侧的表达式进行求值,如果此值能匹配 let
等号左侧的模式,则执行循环体,然后重复执行此过程。如果模式匹配失败,则结束循环,继续执行 while-let
表达式之后的代码。例如:
from std import random.*
// 此函数模拟在通信中接收数据,获取数据可能失败
func recv(): Option<UInt8> {
let number = Random().nextUInt8()
if (number < 128) {
return Some(number)
}
return None
}
main() {
// 模拟循环接收通信数据,如果失败就结束循环
while (let Some(data) <- recv()) {
println(data)
}
println("receive failed")
}
运行以上程序,可能的输出为:
73
94
receive failed
其他使用模式的地方
模式除了可以在 match
表达式中使用外,还可以使用在变量定义(等号左侧是个模式)和 for in 表达式(for
关键字和 in
关键字之间是个模式)中。
但是,并不是所有的模式都能使用在变量定义和 for in
表达式中,只有 irrefutable
的模式才能在这两处被使用,所以只有通配符模式、绑定模式、irrefutable
tuple 模式和 irrefutable
enum 模式是允许的。
-
变量定义和
for in
表达式中使用通配符模式的例子如下:main() { let _ = 100 for (_ in 1..5) { println("0") } }
上例中,变量定义时使用了通配符模式,表示定义了一个没有名字的变量(当然此后也就没办法对其进行访问),
for in
表达式中使用了通配符模式,表示不会将1..5
中的元素与某个变量绑定(当然循环体中就无法访问1..5
中元素值)。编译执行上述代码,输出结果为:0 0 0 0
-
变量定义和
for in
表达式中使用绑定模式的例子如下:main() { let x = 100 println("x = ${x}") for (i in 1..5) { println(i) } }
上例中,变量定义中的
x
以及for in
表达式中的i
都是绑定模式。编译执行上述代码,输出结果为:x = 100 1 2 3 4
-
变量定义和
for in
表达式中使用irrefutable
tuple 模式的例子如下:main() { 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
的值。编译执行上述代码,输出结果为:x = 100 y = 200 Sum = 3 Sum = 7 Sum = 11
-
变量定义和
for in
表达式中使用irrefutable
enum 模式的例子如下:enum 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
的值。编译执行上述代码,输出结果为:red = 0 r = 10 r = 20 r = 30