枚举类型
- 定义 enum 时需要把它所有可能的取值一一列出,我们称这些值为 enum 的构造器(或者 constructor)。enum 类型的定义以关键字 enum 开头,接着是 enum 的名字,之后是定义在一对花括号中的 enum 体,enum 体中定义了若干构造器,多个构造器之间使用 | 进行分隔(第一个构造器之前的 | 是可选的)。
enum RGBColor {
| Red | Green | Blue
}
- enum 中的构造器还可以携带若干(至少一个)参数,称为有参构造器。
enum RGBColor {
| Red(UInt8) | Green(UInt8) | Blue(UInt8)
}
- 仓颉支持同一个 enum 中定义多个同名构造器,但是要求这些构造器的参数个数不同(认为没有参数的构造器的参数个数等于 0)
enum RGBColor {
| Red | Green | Blue
| Red(UInt8) | Green(UInt8) | Blue(UInt8)
}
- enum 支持递归定义,例如,下面的例子中使用 enum 定义了一种表达式(即 Expr),此表达式只能有 3 种形式:单独的一个数字 Num(携带一个 Int64 类型的参数)、加法表达式 Add(携带两个 Expr 类型的参数)、减法表达式 Sub(携带两个 Expr 类型的参数)。对于 Add 和 Sub 这两个构造器,其参数中递归地使用到了 Expr 自身。
enum Expr {
| Num(Int64)
| Add(Expr, Expr)
| Sub(Expr, Expr)
}
- 在 enum 体中还可以定义一系列成员函数、操作符函数和成员属性,但是要求构造器、成员函数、成员属性之间不能重名。
enum RGBColor {
| Red | Green | Blue
public static func printType() {
print("RGBColor")
}
}
- 定义了 enum 类型之后,就可以创建此类型的实例(即 enum 值),enum 值只能取 enum 类型定义中的一个构造器。enum 没有构造函数,可以通过 类型名.构造器,或者直接使用构造器的方式来构造一个 enum 值(对于有参构造器,需要传实参)。
enum RGBColor {
| Red | Green | Blue(UInt8)
}
main() {
let r = RGBColor.Red
let g = Green
let b = Blue(100)
}
- 当省略类型名时,enum 构造器的名字可能和类型名、变量名、函数名发生冲突。此时必须加上 enum 类型名来使用 enum 构造器,否则只会选择同名的类型、变量、函数定义。
let Red = 1
func Green(g: UInt8) {
return g
}
enum RGBColor {
| Red | Green(UInt8) | Blue(UInt8)
}
let r1 = Red // Will choose 'let Red'
let r2 = RGBColor.Red // Ok: constructed by enum type name
let g1 = Green(100) // Will choose 'func Green'
let g2 = RGBColor.Green(100) // Ok: constructed by enum type name
let b = Blue(100) // Ok: can be uniquely identified as an enum constructor
Option类型
- Option 类型使用 enum 定义,它包含两个构造器:Some 和 None。其中,Some 会携带一个参数,表示有值,None 不带参数,表示无值。当需要表示某个类型可能有值,也可能没有值的时候,可选择使用 Option 类型。
enum Option<T> {
| Some(T)
| None
}
- Option 类型还有一种简单的写法:在类型名前加 ?。也就是说,对于任意类型 Ty,?Ty 等价于 Option<Ty>。例如,?Int64 等价于 Option<Int64>,?String 等价于 Option<String> 等等。
let a: Option<Int64> = Some(100)
let b: ?Int64 = Some(100)
let c: Option<String> = Some("Hello")
let d: ?String = None
这些和swift很像,问号的位置好像不一样
- 另外,虽然 T 和 Option<T> 是不同的类型,但是当明确知道某个位置需要的是 Option<T> 类型的值时,可以直接传一个 T 类型的值,编译器会用 Option<T> 类型的 Some 构造器将 T 类型的值封装成 Option<T> 类型的值(注意:这里并不是类型转换)。例如,下面的定义是合法的(等价于上例中变量 a,b 和 c 的定义
let a: Option<Int64> = 100
let b: ?Int64 = 100
let c: Option<String> = "100"
有这种简便写法,谁还会带上Some()
- 在上下文没有明确的类型要求时,无法使用 None 直接构造出想要的类型,此时应使用 None<T> 这样的语法来构造 Option<T> 类型的数据
let a = None<Int64> // a: Option<Int64>
let b = None<Bool> // b: Option<Bool>
模式概述
常量模式
- 常量模式可以是整数字面量、浮点数字面量、字符字面量、布尔字面量、字符串字面量(不支持字符串插值)、Unit 字面量。
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
*/
感觉像Switch,稍有不同
- 在模式匹配的目标是静态类型为 Rune 的值时,Rune 字面量和单字符字符串字面量都可用于表示 Rune 类型字面量的常量 pattern。
func translate(n: Rune) {
match (n) {
case "一" => 1
case "二" => 2
case "三" => 3
case _ => -1
}
}
main() {
println(translate(r"三"))
}
/* 输出结果为:
3
*/
- 在模式匹配的目标是静态类型为 Byte 的值时,一个表示 ASCII 字符的字符串字面量可用于表示 Byte 类型字面量的常量 pattern。
func 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
*/
通配符模式
通配符模式使用下划线 _ 表示,可以匹配任意值。通配符模式通常作为最后一个 case 中的模式,用来匹配其他 case 未覆盖到的情况
绑定模式
绑定模式使用 id 表示,id 是一个合法的标识符。与通配符模式相比,绑定模式同样可以匹配任意值,但绑定模式会将匹配到的值与 id 进行绑定,在 => 之后可以通过 id 访问其绑定的值。
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 进行修改。
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 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 模式。
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
*/
感觉像元组的Switch,其他地方很少见到,感觉比较有用
- 同一个 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"
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),它们的差别是后者会发生变量绑定,而前者并不会。
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
*/
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
*/
类似于swift中的as和is,统一成类似Switch的用法,需要转变一下思路
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 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})"
}
}
match表达式
仓颉支持两种 match 表达式,第一种是包含待匹配值的 match 表达式,第二种是不含待匹配值的 match 表达式。
含匹配值的 match 表达式:
match 表达式以关键字 match 开头,后跟要匹配的值(如例中的 x,x 可以是任意表达式),接着是定义在一对花括号内的若干 case 分支。
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)
}
}
/* 输出结果为:
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 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 表达式
与包含待匹配值的 match 表达式相比,关键字 match 之后并没有待匹配的表达式,并且 case 之后不再是 pattern,而是类型为 Bool 的表达式(上述代码中的 x > 0 和 x < 0)或者 _(表示 true),当然,case 中也不再有 pattern guard。
main() {
let x = -1
match {
case x > 0 => print("x > 0")
case x < 0 => print("x < 0") // Matched.
case _ => print("x = 0")
}
}
/* 输出结果为:
x < 0
*/
match 表达式的类型
- 在上下文有明确的类型要求时,要求每个 case 分支中 => 之后的代码块的类型是上下文所要求的类型的子类型;
let x = 2
let s: String = match (x) { // 定义变量 s 时,显式地标注了其类型为 String,属于上下文类型信息明确的情况,因此要求每个 case 的 => 之后的代码块的类型均是 String 的子类型
case 0 => "x = 0"
case 1 => "x = 1"
case _ => "x != 0 and x != 1" // Matched.
}
- 在上下文没有明确的类型要求时,match 表达式的类型是每个 case 分支中 => 之后的代码块的类型的最小公共父类型
let x = 2
let s = match (x) { // 定义变量 s 时,未显式标注其类型,因为每个 case 的 => 之后的代码块的类型均是 String,所以 match 表达式的类型是 String,进而可确定 s 的类型也是 String。
case 0 => "x = 0"
case 1 => "x = 1"
case _ => "x != 0 and x != 1" // Matched.
}
if-let表达式
if-let 表达式首先对条件中 <- 右侧的表达式进行求值,如果此值能匹配 <- 左侧的模式,则执行 if 分支,否则执行 else 分支(可省略)。
main() {
let result = Option<Int64>.Some(2023)
if (let Some(value) <- result) {
println("操作成功,返回值为:${value}")
} else {
println("操作失败")
}
}
/* 输出结果为:
操作成功,返回值为:2023
*/
和Swift的if let有点像,都是取出可选类型的值。不过细节上又有所不同
- 如果是None,将匹配失败,也就是没有值的情况:
main() {
let result = Option<Int64>.None
if (let Some(value) <- result) {
println("操作成功,返回值为:${value}")
} else {
println("操作失败")
}
}
/* 输出结果为:
操作失败
*/
while-let表达式
while-let 表达式首先对条件中 <- 右侧的表达式进行求值,如果此值能匹配 <- 左侧的模式,则执行循环体,然后重复执行此过程。如果模式匹配失败,则结束循环,继续执行 while-let 表达式之后的代码。
import std.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
*/
while循环中的if let,用起来更简洁一点
其他使用模式的地方
模式除了可以在 match 表达式中使用外,还可以使用在变量定义(等号左侧是个模式)和 for in 表达式(for 关键字和 in 关键字之间是个模式)中。
- 变量定义和 for in 表达式中使用通配符模式的例子
main() {
let _ = 100 // 变量定义时使用了通配符模式,表示定义了一个没有名字的变量(当然此后也就没办法对其进行访问)
for (_ in 1..5) { // for in 表达式中使用了通配符模式,表示不会将 1..5 中的元素与某个变量绑定(当然循环体中就无法访问 1..5 中元素值)
println("0")
}
}
/* 输出结果为:
0
0
0
0
*/
- 变量定义和 for in 表达式中使用绑定模式的例子
main() {
/// 变量定义中的 x 以及 for in 表达式中的 i 都是绑定模式
let x = 100
println("x = ${x}")
for (i in 1..5) {
println(i)
}
}
/* 输出结果为:
x = 100
1
2
3
4
*/
- 变量定义和 for in 表达式中使用 irrefutable tuple 模式的例子
main() {
let (x, y) = (100, 200) // 变量定义时使用了 tuple 模式,表示对 (100, 200) 进行解构并分别和 x 与 y 进行绑定,效果上相当于定义了两个变量 x 和 y。
println("x = ${x}")
println("y = ${y}")
for ((i, j) in [(1, 2), (3, 4), (5, 6)]) { // for in 表达式中使用了 tuple 模式,表示依次将 [(1, 2), (3, 4), (5, 6)] 中的 tuple 类型的元素取出,然后解构并分别和 i 与 j 进行绑定,循环体中输出 i + j 的值。
println("Sum = ${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) // 变量定义时使用了 enum 模式,表示对 Red(0) 进行解构并将构造器的参数值(即 0)与 red 进行绑定。
println("red = ${red}")
for (Red(r) in [Red(10), Red(20), Red(30)]) { // for in 表达式中使用了 enum 模式,表示依次将 [Red(10), Red(20), Red(30)] 中的元素取出,然后解构并将构造器的参数值与 r 进行绑定,循环体中输出 r 的值。
println("r = ${r}")
}
}
/* 输出结果为:
red = 0
r = 10
r = 20
r = 30
*/