4639 字
23 分钟
仓颉文档阅读-开发指南VI: 枚举类型和模式匹配(II) - 模式的Refutability、match表达式 与 其他使用模式的地方
NOTE

阅读文档版本:

语言规约 Cangjie-0.53.18-Spec

具体开发指南 Cangjie-LTS-1.0.4

在阅读 了解仓颉的语言规约时, 难免会涉及到一些仓颉的示例代码, 但 我们对仓颉并不熟悉, 所以可以用 仓颉在线体验 快速验证

有条件当然可以直接 配置 Canjie-SDK

WARNING

博主在此之前, 基本只接触过 C/C++语言, 对大多现代语言都没有了解, 所以在阅读过程中遇到相似的概念, 难免会与 C/C++中的相似概念作类比, 见谅

且, 本系列是文档阅读, 而不是仓颉的零基础教学, 所以如果要跟着阅读的话最好有一门编程语言的开发经验

WARNING

在阅读仓颉编程语言的开发指南之前, 已经大概阅读了一遍 仓颉编程语言的语言规约, 已经对仓颉编程语言有了一个大概的了解

所以在阅读开发指南时, 不会对类似: 类、函数、结构体、接口等解释起来较为复杂名称 做出解释

此样式内容, 表示文档原文内容

枚举类型和模式匹配#

模式的Refutability#

模式可以分为两类: refutable模式和irrefutable模式

类型匹配的前提下, 当一个模式有可能和待匹配值不匹配时, 称此模式为refutable模式; 反之, 当一个模式总是可以和待匹配值匹配时, 称此模式为irrefutable模式

Refutability翻译为可证伪性

仓颉将模式分为两类: 可证伪(refutable) 和 不可证伪(irrefutable)

并解释, 在类型匹配的前提下:

  1. refutable可证伪, 表示 待匹配值可能存在无法匹配成功的情况

  2. irrefutable不可证伪, 表示 待匹配值总能匹配成功的情况

对于上述介绍的各种模式, 规定如下:

常量模式是refutable模式

例如, 下例中第一个case中的1和 第二个case中的2都有可能和x的值不相等

func constPat(x: Int64) {
match (x) {
case 1 => "one"
case 2 => "two"
case _ => "_"
}
}

常量模式一定为refutable的, 即 可证伪的

因为你无法列举出所有的字面量, 更别说一定匹配成功了

通配符模式是irrefutable模式

例如, 下例中无论x的值是多少, _总能和其匹配

func wildcardPat(x: Int64) {
match (x) {
case _ => "_"
}
}

绑定模式是irrefutable模式

例如, 下例中无论x的值是多少, 绑定模式a总能和其匹配

func varPat(x: Int64) {
match (x) {
case a => "x = ${a}"
}
}

通配符模式和绑定模式, 是irrefutable不可证伪的

因为他们总能匹配任意值

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})"
}
}

Tuple模式, 可以是refutable可证伪的, 也可以是irrefutable不可证伪的

因为Tuple模式依赖于其他模式, 所以 当且仅当Tuple模式包含的每个模式都是irrefutable的时, Tuple模式是irrefutable

类型模式是refutable模式

例如, 下例中(假设BaseDerived的父类, 并且Base实现了接口I), x的运行时类型有可能既不是Base也不是Derived, 所以a: Derivedb: 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"
}
}

类型模式是refutable可证伪的

因为, 可能存在待匹配目标为接口类型, 此时可能存在match中无法匹配待匹配目标的类型

enum模式是irrefutable模式, 当且仅当它对应的enum类型中只有一个有参构造器, 且enum模式中包含的其他模式也是irrefutable模式

例如, 对于下例中的E1E2定义, 函数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})"
}
}

enum模式 与Tuple模式类似, 都 可以是refutable可证伪的, 也可以是irrefutable不可证伪的, 因为他们都是要依赖其他模式的

enum还有一点不同的是, match表达式中enum模式通常需要覆盖目标enum类型的所有构造器

所以enum如果要是irrefutable的 还有其他条件, 当且仅当enum类型中只有一个有参构造器, 且包含的每个模式都是irrefutable的时, enum模式是irrefutable

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之后是一个模式或多个由|连接的相同种类的模式(如上例中的10_都是模式, 详见 模式概述 章节)

match表达式的语法即为:

match (待匹配值) {
case 模式1 => ""
case 模式2 | 模式2 => ""
}

match表达式的case分支是用于匹配模式的, case之后的模式 可以使用|连接同种模式

模式之后可以接一个可选的pattern guard, 表示本条case匹配成功后额外需要满足的条件, pattern guard使用where cond表示, 要求表达式cond的类型为Bool

接着是一个=>, =>之后即本条case分支匹配成功后需要执行的操作, 可以是一系列表达式、变量和函数定义(新定义的变量或函数的作用域从其定义处开始到下一个case之前结束), 如上例中的变量定义和print函数调用

match表达式执行时, 依次match之后的表达式与每个case中的模式进行匹配

一旦匹配成功, 则执行=>之后的代码, 然后退出match表达式的执行(意味着不会再去匹配它之后的case)

关于匹配成功, 如果有pattern guard, 也需要where之后的表达式的值为true; 如果case中有多个由|连接的模式, 只要待匹配值和其中一个模式匹配, 则认为匹配成功

如果匹配不成功, 则继续与它之后的case中的模式进行匹配, 直到匹配成功(match表达式可以保证一定存在匹配的case分支)

上例中, 因为x的值等于0, 所以会和第二条case分支匹配(此处使用的是常量模式, 匹配的是值是否相等, 详见 常量模式 章节), 最后输出x = 0

编译并执行上述代码, 输出结果为:

x = 0

case模式之后, 还可以接一个pattern guard, 即where cond

所以, match表达式的语法可以是:

match (待匹配值) {
case 模式1 where 条件 => ""
}

pattern guard中的条件, 必须是Bool类型

在匹配成功之后, 才会进行pattern guard判断, 如果模式是绑定模式, 那么条件中可以使用绑定的变量

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")
}
}

如果被匹配值的类型包含enum类型, 且该enumnon-exhaustive enum(包含无名构造器的enum), 则其在匹配时 需要使用可匹配所有构造器的模式, 如通配符模式_和绑定模式

enum T {
| Red | Green | Blue | ...
}
func foo(a: T) {
match (a) {
case Red => 0
case Green => 1
case Blue => 2
case _ => -1
}
}
func bar(a: T) {
match (a) {
case Red => 0
case k => -1 // 简单绑定模式
}
}
func baz(a: T) {
match (a) {
case Red => 0
case k: T => -1 // 带嵌套类型模式的绑定模式
}
}

仓颉match表达式, 要求待匹配项必须能够匹配成功, 即 待匹配表达式的所有可能取值都应该被考虑到

即 使用match表达式进行模式匹配, 必须存在至少一个case分支能够匹配成功

如果待匹配项是enum类型, 那么同样需要手动匹配所有构造器, 如果enum存在...无名构造器, 一般就用 通配符_模式 或 绑定模式

当然, 其他类型也可以使用通配符_模式 和 绑定模式进行 匹配所有可能的取值

case分支的模式之后, 可以使用pattern guard进一步对匹配出来的结果进行判断

在下面的例子中(使用到了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

pattern guard是在匹配成功之后才进行判断的

没有匹配值的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 > 0x < 0)或者_(表示true), 当然, case中也不再有pattern guard

无匹配值的match表达式执行时, 依次判断case之后的表达式的值, 直到遇到值为truecase分支:

一旦某个case之后的表达式值等于true, 则执行此case=>之后的代码, 然后退出match表达式的执行(意味着不会再去判断该case之后的其他case)

上例中, 因为x的值等于-1, 所以第二条case分支中的表达式(即x < 0)的值等于true, 执行print("x < 0")

编译并执行上述代码, 输出结果为:

x < 0

从文档来看, 无匹配值的match表达式, 更大的用处是 针对可见变量的分支判断, 实际类似if-else

文档中的例子, 实际可以等价于:

let x = -1
if (x > 0) {
print("x > 0")
}
else if (x < 0) {
print("x < 0")
}
else {
print("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表达式的类型, 与仓颉其他表达式的类型是一致的

如果match表达式的值被使用了:

  1. 如果上下文有要求, 每个case分支 的最终类型都要是上下文要求的子类型

  2. 如果没有要求, 那么 就是每个case分支的最小公共父类型

如果没有被使用, 就没有任何要求, 类型就是Unit

其他使用模式的地方#

模式除了可以在match表达式中使用外, 还可以使用在变量定义(等号左侧是一个模式)和 for in表达式(for关键字和in关键字之间是一个模式)中

但是, 并不是所有的模式都能使用在变量定义和for in表达式中, 只有irrefutable的模式才能在这两处被使用, 所以只有通配符模式、绑定模式、irrefutable tuple模式和irrefutable enum模式是允许的

仓颉的模式, 并不是只能在match表达式中使用, 还可以使用在变量定义和for-in表达式

但只有不可证伪的模式才能在其他地方使用, 即 通配符_模式, 绑定模式 以及 不可证伪的Tuple以及enum模式

定义变量时, =左边是一个模式(变量名)

for-in表达式中, forin之间是一个模式

  1. 变量定义和for in表达式中使用通配符模式的例子如下:

    main() {
    let _ = 100
    for (_ in 1..5) {
    println("0")
    }
    }

    上例中, 变量定义时使用了通配符模式, 表示定义了一个没有名字的变量(当然此后也就没办法对其进行访问)

    for in表达式中使用了通配符模式, 表示不会将1..5中的元素与某个变量绑定(当然循环体中就无法访问1..5中元素值)

    编译执行上述代码, 输出结果为:

    0
    0
    0
    0

定义变量, 可以使用通配符替代普通变量, 表示忽略目标

for-in表达式, forin之间也可以使用通配符, 也表示忽略目标

  1. 变量定义和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表达式最基础的使用方式, 也是一个绑定模式

  1. 变量定义和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)进行解构并分别和xy进行绑定, 效果上相当于定义了两个变量xy

    for in表达式中使用了tuple模式, 表示依次将[(1, 2), (3, 4), (5, 6)]中的tuple类型的元素取出, 然后解构并分别和ij进行绑定, 循环体中输出i + j的值

    编译执行上述代码, 输出结果为:

    x = 100
    y = 200
    Sum = 3
    Sum = 7
    Sum = 11

irrefutable Tuple模式, 就是只包含通配符_模式和绑定模式的Tuple模式

定义变量时, 可以使用irrefutable Tuple模式, 但此时并不是简单的通配符模式或绑定模式, 同时包含元组的解构

let (x, y) = (100, 200)
println("x = ${x}")
println("y = ${y}")

并不是简单定义了一个元组, 而是 在定义变量时 通过irrefutable Tuple模式, 元组模式匹配 并 解构元组, 定义了两个不同的变量

for ((i, j) in [(1, 2), (3, 4), (5, 6)]) {
println("Sum = ${i + j}")
}

for-in表达式中也是同样的概念

  1. 变量定义和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

irrefutable enum模式与irrefutable Tuple是类似的, 包含模式匹配 与 解构