模式匹配

2025-10-29 22:10:43
模式匹配 本章主要介绍仓颉中的模式匹配(pattern matching),首先介绍 match 表达式和模式,然后介绍模式的 refutability(即某个模式是否一定能匹...

模式匹配

本章主要介绍仓颉中的模式匹配(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 类型的Some 构造器 Option.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.Some(2023)

if (let Some(value) <- result) {

println("操作成功,返回值为:${value}")

} else {

println("操作失败")

}

}

运行以上程序,将输出:

操作成功,返回值为:2023

对于以上程序,如果将 result 的初始值修改为 Option.None,则 if-let 的模式匹配会失败,将执行 else 分支:

main() {

let result = Option.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 {

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