1. 控制流运算符match
Rust中有一个异常强大的控制流运算符:match,它允许将一个值与一系列的模式相比较,并根据匹配的模式执行相应代码。模式可由字面量、变量名、通配符和许多其他东西组成;后文会详细介绍所有不同种类的模式及它们的工作机制。
match的能力不仅来自模式丰富的表达力,也来自编译器的安全检查,它确保了所有可能的情况都会得到处理。你可以将match表达式想象成一台硬币分类机:硬币滑入有着不同大小孔洞的轨道,并且掉入第一个符合大小的孔洞。
同样,值也会依次通过match中的模式,并且在遇到第一个“符合”的模式时进入相关联的代码块,并在执行过程中被代码所使用。
由于我们正好提到了硬币,所以就用它们来编写一个使用match的示例!示例中的函数会接收一个美国的硬币作为输入,并以一种类似于验钞机的方式,确定硬币的类型并返回它的分值,如示例6-3所示。
// 示例6-3:一个枚举以及一个以枚举变体作为模式的match表达式
❶enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u32 {
❷ match coin {
❸ Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
让我们先来逐步分析一下函数value_in_cents中的match块。首先,我们使用的match关键字后面会跟随一个表达式,也就是本例中的coin值❷。
初看上去,这与if表达式的使用十分相似,但这里有个巨大的区别:在if语句中,表达式需要返回一个布尔值,而这里的表达式则可以返回任何类型。
例子中coin的类型正是我们在首行❶中定义的Coin枚举。接下来是match的分支,一个分支由模式和它所关联的代码组成。第一个分支采用了值Coin::Penny作为模式,并紧跟着一个=>运算符用于将模式和代码区分开来❸。
这里的代码简单地返回了值1。不同分支之间使用了逗号分隔。当这个match表达式执行时,它会将产生的结果值依次与每个分支中的模式相比较。
假如模式匹配成功,则与该模式相关联的代码就会被继续执行。而假如模式匹配失败,则会继续执行下一个分支,就像上面提到过的硬币分类机一样。分支可以有任意多个,在示例6-3中,match有4个分支。
每个分支所关联的代码同时也是一个表达式,而这个表达式运行所得到的结果值,同时也会被作为整个match表达式的结果返回。
如果分支代码足够短,就像示例6-3中仅返回一个值的话,那么通常不需要使用花括号。但是,假如我们想要在一个匹配分支中包含多行代码,那么就可以使用花括号将它们包裹起来。
例如,下面的代码会在每次给函数传入Coin::Penny时打印“Lucky penny!”,同时仍然返回代码块中最后的值1:
fn value_in_cents(coin: Coin) -> u32 {
match coin {
Coin::Penny => {
println!("Lucky penny!");
1
},
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
2. 绑定值的模式
匹配分支另外一个有趣的地方在于它们可以绑定被匹配对象的部分值,而这也正是我们用于从枚举变体中提取值的方法。
下面举一个例子,让我们修改上面的枚举变体来存放数据。在1999年到2008年之间,美国在25美分硬币的一侧为50个州采用了不同的设计。其他类型的硬币都没有类似的各州的设计,所以只有25美分拥有这个特点。
我们可以通过在Quarter变体中添加一个UsState值,来将这些信息添加至枚举中,如示例6-4所示。
// 示例6-4:Coin枚举中的Quarter变体存放了一个UsState值
#[derive(Debug)] // 使我们能够打印并观察各州的设计
enum UsState {
Alabama,
Alaska,
// --略--
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
假设我们有一个朋友正在尝试收集所有50个州的25美分硬币。当我们在根据硬币类型进行大致分类的时候,也可以打印出每个25美分硬币所对应的州的名字。
一旦这个朋友发现了没有的硬币,就可以将其加入自己的收藏中。在这份代码的匹配表达式中,我们在模式中加入了一个叫作state的变量用于匹配变体Coin::Quarter中的值。
当匹配到Coin::Quarter时,变量state就会被绑定到25美分所包含的值上。接着,我们就可以在这个分支中像下面一样使用state了:
fn value_in_cents(coin: Coin) -> u32 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => {
println!("State quarter from {:?}!", state);
25
},
}
}
如果我们在代码中调用value_in_cents(Coin::Quarter(UsState:: Alaska)),Coin::Quarter(UsState::Alaska)就会作为coin的值传入函数。
这个值会依次与每个分支进行匹配,一直到Coin:: Quarter(state)模式才会终止匹配。这时,值UsState::Alaska就会被绑定到变量state上。
接着,我们就可以在println! 表达式中使用这个绑定了,这就是从Coin枚举的变体Quarter中获取值的方法。
3. 匹配Option<T>
在上篇文章中,我们曾经想要在使用Option<T>时,从Some中取出内部的T值;现在我们就可以如同操作Coin枚举一样,使用match来处理Option<T>了!
除了使用Option<T>的变体而不是Coin的变体来进行比较,match表达式的大部分工作流程完全一致。
比如,我们想要编写一个接收Option<i32>的函数,如果其中有值存在,则将这个值加1。如果其中不存在值,那么这个函数就直接返回None而不进行任何操作。
得益于match方法的使用,编写这个函数将会非常简单,它看起来会如示例6-5所示:
// 示例6-5:一个对Option<i32>使用match表达式的函数
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
❶ None => None,
❷ Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five);❸
let none = plus_one(None);❹
让我们来分析一下首次执行plus_one的过程中究竟发生了些什么。当我们调用plus_one(five)❸时,plus_one函数体中的变量x被绑定为值Some(5)。随后我们会将这个值与各个分支进行比较。
自然,Some(5)没办法匹配上模式None❶,所以我们继续尝试与下一个分支进行比较。❷这里Some(5)会匹配上Some(i)吗?答案是肯定的!匹配的两端拥有相同的变体。
这里的i绑定了Some所包含的值,也就是5。接着,这个匹配分支中的代码得到执行,我们将i中的值加1,并返回一个新的包含了结果为6的Some值。
现在,再让我们来看一看示例6-5中plus_one的第二次调用,这一次,x变成了None❹。依然继续进入match表达式,并将它与第一个分支❶进行比较。
它们匹配上了!这里我们没有可用于增加的对象,所以=>右侧的程序会简单地终止并返回None值。由于第一个分支匹配成功,因此其他的分支会被跳过。
将match与枚举相结合在许多情形下都是非常有用的。你会在Rust代码中看到许多类似的套路:使用match来匹配枚举值,并将其中的值绑定到某个变量上,接着根据这个值执行相应的代码。
这初看起来可能会有些复杂,不过一旦你习惯了它的用法,就会希望在所有的语言中都有这个特性。这一特性一直以来都是社区用户的最爱。
4. 匹配必须穷举所有的可能
match表达式中还有另外一个需要注意的特性。你可以先来看下面这个存在bug、无法编译的plus_one函数版本:
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
Some(i) => Some(i + 1),
}
}
此段代码的问题在于我们忘记了处理值是None的情形。幸运的是,这是一个Rust可以轻松捕获的问题。假如我们尝试去编译这段代码,就会看到如下所示的错误提示信息:
error[E0004]: non-exhaustive patterns: `None` not covered
-->
|
6 | match x {
| ^ pattern `None` not covered
Rust知道我们没有覆盖所有可能的情形,甚至能够确切地指出究竟是哪些模式被我们漏掉了!
Rust中的匹配是穷尽的(exhausitive):我们必须穷尽所有的可能性,来确保代码是合法有效的。
特别是在这个Option<T>的例子中,Rust会强迫我们明确地处理值为None的情形。这使得我们不需要去怀疑所持有值的存在性,因而可以有效地避免前面提到过的10亿美金的错误。
5. _通配符
有的时候,我们可能并不想要处理所有可能的值,Rust同样也提供了一种模式用于处理这种需求。
例如,一个u8可以合法地存储从0到255之间的所有整数。但假设我们只关心值为1、3、5或7时的情形,我们就没有必要去列出0、2、4、6、8、9直到255等其余的值。
所幸我们也确实可以避免这种情形,即通过使用一个特殊的模式_来替代其余的值:
let some_u8_value = 0u8;
match some_u8_value {
1 => println!("one"),
3 => println!("three"),
5 => println!("five"),
7 => println!("seven"),
_ => (),
}
这里的_模式可以匹配任何值。通过将它放置于其他分支后,可以使其帮我们匹配所有没有被显式指定出来的可能的情形。
与它对应的代码块里只有一个()空元组,所以在_匹配下什么都不会发生。使用它也就暗示了,我们并不关心那些在_通配符前没有显式列出的情形,且不想为这些情形执行任何操作。
不过,在只关心某一种特定可能的情形下,使用match仍然会显得有些烦琐。为此,Rust提供了if let语句。