match控制流结构
Cairo 有一个叫做 match
的极为强大的控制流运算符,它允许我们将一个值与一系列的模式相比较,并根据相匹配的模式执行相应代码。模式可由字面值、变量、通配符和许多其他内容构成;第十八章会涉及到所有不同种类的模式以及它们的作用。match
的力量来源于模式的表现力以及编译器检查,它确保了所有可能的情况都得到处理。
可以把 match
表达式想象成某种硬币分类器:硬币滑入有着不同大小孔洞的轨道,每一个硬币都会掉入符合它大小的孔洞。同样地,值也会通过 match
的每一个模式,并且在遇到第一个 “符合” 的模式时,值会进入相关联的代码块并在执行中被使用。
因为刚刚提到了硬币,让我们用它们来作为一个使用 match
的例子!我们可以编写一个函数来获取一个未知的硬币,并以一种类似验钞机的方式,确定它是何种硬币,并返回其价值(美分),如示例6-3所示。
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> felt252 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
示例6-3:一个枚举和一个将枚举成员作为其模式的 match 表达式
拆开 value_in_cents
函数中的 match
来看。首先,我们列出 match
关键字后跟一个表达式,在这个例子中是 coin 的值。这看起来非常像 if
所使用的条件表达式,不过这里有一个非常大的区别:对于 if
,表达式必须返回一个布尔值,而这里它可以是任何类型的。本例中硬币的类型是我们在第一行定义的 Coin
枚举。
接下来是 match
分支。一个分支有两个部分:一个模式和一些代码。这里的第一个分支有一个模式,就是值 Coin::Penny(_)
,然后是=>
操作符,把模式和要运行的代码分开。本例中的代码只是值 1
。每个分支都用逗号与下一个分支隔开。
当 match
表达式执行时,它将结果值与每个分支的模式进行比较,依次进行。如果一个模式与值匹配,则执行与该模式相关的代码。如果该模式不匹配该值,则继续执行下一个分支,就像验钞机一样。我们可以根据自己的需要有多少个分支:在上面的例子中,我们的匹配有四个分支。
在 Cairo,分支的顺序必须遵循与枚举相同的顺序。
与每个 match 相关的代码是一个表达式,而 match 分支中表达式的结果值是整个 match 表达式被返回的值。
如果 match 分支的代码很短,我们通常不使用大括号,就像在我们的示例中一样,每个分支只是返回一个值。如果你想在一个 match 分支中运行多行代码,你必须使用大括号,在 match 分支后面加一个逗号。例如,下面的代码在每次调用 Coin::Penny
方法时都会打印出 “Lucky penny!”,但仍然返回区块的最后一个值 1
:
fn value_in_cents(coin: Coin) -> felt252 {
match coin {
Coin::Penny => {
('Lucky penny!').print();
1
},
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
绑定值的模式
另一个match分支的有用的特点是它们可以绑定到值中与模式相匹配的部分。这就是如何从枚举成员中提取数值的方法。
作为一个例子,让我们修改枚举的一个成员来存放数据。1999 年到 2008 年间,美国在 25 美分的硬币的一侧为 50 个州的每一个都印刷了不同的设计。其他的硬币都没有这种区分州的设计,所以只有这些 25 美分硬币有特殊的价值。我们可以通过改变 Quarter
变量,使其包含一个UsState
的值,从而将这个信息添加到我们的 enum
中,我们在示例6-4中已经这样做了。
#[derive(Drop)]
enum UsState {
Alabama,
Alaska,
}
#[derive(Drop)]
enum Coin {
Penny,
Nickel,
Dime,
Quarter: UsState,
}
列表6-4: 一个 Coin
枚举,其中 Quarter
变量也持有一个 UsState
值
想象一下我们的一个朋友尝试收集所有 50 个州的 25 美分硬币。在根据硬币类型分类零钱的同时,也可以报告出每个 25 美分硬币所对应的州名称,这样如果我们的朋友没有的话,他可以将其加入收藏。
在这段代码的match表达式中,我们在模式中添加了一个叫做 state
的变量,用来匹配变量 Coin::Quarter
的值。当 Coin::Quarter
匹配时,state
变量将与该季度的状态值绑定。然后我们可以在该分支的代码中使用 state
,像这样:
fn value_in_cents(coin: Coin) -> felt252 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => {
state.print();
25
},
}
}
为了在 Cairo 打印一个枚举的变量的值,我们需要为debug::PrintTrait
添加一个 print
函数的实现:
impl UsStatePrintImpl of PrintTrait<UsState> {
fn print(self: UsState) {
match self {
UsState::Alabama => ('Alabama').print(),
UsState::Alaska => ('Alaska').print(),
}
}
}
如果我们调用 value_in_cents(Coin::quarter(UsState::Alaska))
,coin
将是 Coin::quarter(UsState::Alaska)
。当我们将该值与每个匹配臂进行比较时,没有一个匹配,直到我们到达 Coin::Quarter(state)
。在这一点上,state 的绑定将是 UsState::Alaska
的值。然后我们可以在 PrintTrait
中使用该绑定,从而从 Coin
枚举变量中获得 Quarter
的内部状态值。
匹配 Option
在上一节中,我们想在使用 Option<T>
时从 Some
情况下得到内部的 T
值;我们也可以使用match
来处理 Option<T>
,就像我们对 Coin
枚举所做的那样! 只不过这回比较的不再是硬币,而是比较 Option<T>
的成员,但 match
表达式的工作方式保持不变。你可以通过导入 option::OptionTrait
trait来使用Option。
假设我们想写一个函数,接收一个 Option<u8>
,如果里面有一个值,就把 1
加到这个值上。如果里面没有一个值,这个函数应该返回 None
值,并且不试图执行任何操作。
这个函数非常容易编写,这要归功于 match,它看起来像示例6-5。
use debug::PrintTrait; fn plus_one(x: Option<u8>) -> Option<u8> { match x { Option::Some(val) => Option::Some(val + 1), Option::None => Option::None, } } fn main() { let five: Option<u8> = Option::Some(5); let six: Option<u8> = plus_one(five); six.unwrap().print(); let none = plus_one(Option::None); none.unwrap().print(); }
示例6-5:一个在Option<u8>
上使用匹配表达式的函数
注意,你的分支顺序与核心Cairo库的OptionTrait
中定义的枚举顺序必须相同。
enum Option<T> {
Some: T,
None,
}
让我们更详细地看一下 plus_one
的第一次执行。当我们调用 plus_one(five)
时,plus_one
函数体中的变量 x
的值是 Some(5)
。然后我们将其与每个匹配分支进行比较:
Option::Some(val) => Option::Some(val + 1),
Option::Some(5)
和Option::Some(val)
匹配吗?当然匹配!它们是相同的成员。val
与 Option::Some
中包含的值绑定,所以 val
取值为 5
。接着匹配分支的代码被执行,所以我们在 val
的值上加上 1
,并创建一个新的 Option::Some
值,里面有我们的和 6
。因为第一个分支就匹配到了,其他的分支将不再进行比较。
现在让我们考虑在我们的主函数中对 plus_one
的第二次调用,其中 x
是Option::None
。我们进入匹配,并与第一个分支进行比较:
Option::Some(val) => Option::Some(val + 1),
Option::Some(val)
的值不匹配 Option::None
,所以我们继续到下一个分支:
#![allow(unused)] fn main() { Option::None => Option::None, }
它是匹配的!没有值可以添加,所以程序停止,并返回 =>
右边的 Option::None
值。
将 match
与枚举相结合在很多场景中都是有用的。你会在 Cairo 代码中看到很多这样的模式:match
一个枚举,绑定其中的值到一个变量,接着根据其值执行代码。这在一开始有点复杂,不过一旦习惯了,你会希望所有语言都拥有它!这一直是用户的最爱。
匹配是穷尽的
还有另一方面需要讨论:这些分支必须覆盖了所有的可能性。考虑一下 plus_one
函数的这个版本,它有一个 bug 并不能编译:
fn plus_one(x: Option<u8>) -> Option<u8> {
match x {
Option::Some(val) => Option::Some(val + 1),
}
}
$ scarb cairo-run
error: Unsupported match. Currently, matches require one arm per variant,
in the order of variant definition.
--> test.cairo:34:5
match x {
^*******^
Error: failed to compile: ./src/test.cairo
Rust 知道我们没有覆盖所有可能的情况甚至知道哪些模式被忘记了!Cairo 中的匹配是 穷尽的(exhaustive):必须穷举到最后的可能性来使代码有效。特别的在这个 Option<T>
的例子中,Cairo 防止我们忘记明确的处理 None 的情况,这让我们免于假设拥有一个实际上为空的值,从而使之前提到的价值亿万的错误不可能发生。
Match 0 与 _ 占位符
使用枚举,我们也可以对一些特定的值采取特殊的操作,但对所有其他的值采取一个默认的操作。目前只支持 0
和 _
操作符。
想象一下,我们正在实现一个游戏,你会获得一个 0 到 7 之间的随机数字。如果你有 0,你就赢了。若是任何其他的值,你就输了。这里有一个实现该逻辑的 match,数字是硬编码的,而不是随机值。
fn did_i_win(nb: felt252) {
match nb {
0 => ('You won!').print(),
_ => ('You lost...').print(),
}
}
第一条分支,模式是字面值 0。对于涵盖了所有其他可能的值的最后一条分支,其模式是字符 _
。尽管我们没有列出 felt252
可能具有的所有值,但这段代码仍然可以编译,因为最后一个模式将匹配所有没有特别列出的值。这个通配模式满足了 match
必须被穷尽的要求。请注意,我们必须把通配分支放在最后,因为模式是按顺序评估的。如果我们把通配分支放在前面,其他的分支就不会运行,所以如果我们在通配分支之后添加分支,Cairo 会警告我们!