写点什么

Rust 从 0 到 1- 模式 - 使用场景

用户头像
关注
发布于: 2 小时前
Rust从0到1-模式-使用场景

模式(patterns)是 Rust 中重要的语法,用来与不同类型结构的数据相匹,不管其简单还是复杂。将模式与 match 表达式或其它语句结构(如 if 等)结合使用可以提供更多对程序控制流(前面章节介绍过控制流的概念)的控制。模式通常包含以下部分(不需要全部包含):

  • 文本

  • 解构数组、枚举、结构体或者元组

  • 变量

  • 通配符

  • 占位符

它们描述了我们要匹配的数据是什么样子,我们可以通过判断数据是否与之相匹配来决定是否执行程序中的某段代码。也就是说,我们通过将一些数据与模式相比较来使用它。如果数据与模式相匹配,我们就对其进行相应处理,譬如前面章节中讨论 match 表达式时,硬币分类器的例子。如果数据符合模式描述的样子,就可以使用;如果不符合,那么与该模式相关的代码则不会被运行。

本章涉及到了和模式相关的所有内容。包括其使用的场景,refutable(可反驳) 与 irrefutable(不可反驳) 模式的区别,以及不同的模式相关的语法。在阅读完本章后,你将学会如何利用模式编写简洁的代码。

模式出现在 Rust 的很多地方,你已经在无意间使用过很多次!下面我们先来介绍使用到模式的所有场景。

match 分支

在前面章节我们介绍 match 的时候,我们在 match 表达式的分支中使用了模式。用法上 match 表达式由关键字 match、用于匹配的值以及一个或多个分支构成,每个分支包含了一个模式和匹配该模式时执行的表达式:

match VALUE {    PATTERN => EXPRESSION,    PATTERN => EXPRESSION,    PATTERN => EXPRESSION,}
复制代码

match 表达式的一个限制是其必须是穷尽(exhaustive)的,也就是说 match 表达式需要能匹配所有可能的值。一个可以确保这一点的做法是在最后的分支上使用可以匹配任意值的模式:譬如一个变量名,它可以匹配任何值,因此可以覆盖所有其它可能的值。

此外,还有一个特殊的模式 _ 可以匹配任何值,但是它不绑定任何变量,也经常被用在最后的分支上。这适用于我们只关注之前分支中的模式而希望忽略其它可能的情况时。我们在本章后续的内容中会对它进行更为详细的讨论。

条件表达式 if let 

在前面的章节中我们作为 match 表达式的一种简写(只关心一种情况的时候),已经介绍过 if let 表达式。此外,if let 后还可以跟一个 let 语句用于处理 if let 不匹配的情况。在下面的例子中我们展示了也可以将 if let、else if 和 else if let 表达式组合起来进行匹配,这与 match 表达式一次只能将一个值与模式比较相比更加灵活,对于 if let、else if 和 else if let 分支中的条件,并没有要求它们之间存在什么联系。参考下面的例子:

fn main() {    let favorite_color: Option<&str> = None;    let is_tuesday = false;    let age: Result<u8, _> = "34".parse();
if let Some(color) = favorite_color { println!("Using your favorite color, {}, as the background", color); } else if is_tuesday { println!("Tuesday is green day!"); } else if let Ok(age) = age { if age > 30 { println!("Using purple as the background color"); } else { println!("Using orange as the background color"); } } else { println!("Using blue as the background color"); }
复制代码

在上面的例子中,如果用户指定了喜欢的颜色,将使用它作为背景颜色;否则如果今天是星期二,将使用绿色作为背景;否则如果可以正确的得到用户的年龄,将根据年龄大小使用紫色或者橙色作为背景;最后,如果以上条件都不符合,将使用蓝色作为背景。通过这种组合让我们可以支持复杂的需求,例子最终会打印出 Using purple as the background color。

另外,if let 也可以像 match 分支那样使用影子变量(shadowed variables):if let Ok(age) = age 便使用了影子变量 age,它的值是 Ok (在开始我们定义的 age)中的元素。因此,我们需要将 if age > 30 放在分支执行的代码块内部:我们不能将两个条件组合写为 if let Ok(age) = age && age > 30,因为我们用来与 30 进行比较的影子变量 age 只在大括号的作用域内才是有效的。

if let 表达式的缺点在于编译器不会检查其是否是穷尽的,而对于 match 表达式则会检查。也就是说如果我们不在最后加上 else 而遗漏了对一些场景的处理,编译器是不会提示我们可能的逻辑错误的。

条件循环 while let

while let 与 if let 类似,只要模式匹配,它就会一直循环。参考下面的例子:

let mut stack = Vec::new();
stack.push(1);stack.push(2);stack.push(3);
while let Some(top) = stack.pop() { println!("{}", top);}
复制代码

上面的例子使用 vector 作为栈并以先进后出的方式打印出 vector 中的值,它最终顺序打印出的值是 3、2 、1。在 statck 中的所有元素都 pop 出栈后,pop 方法将返回 None。while 循环只要 pop 方法返回 Some 就会一直运行,否则循环停止。

for 循环

for 循环是 Rust 中很常见的循环结构,前面我们已经介绍过,不过没有讨论过它是如何使用模式的。在 for 循环中,模式是紧跟在关键字 for 之后的部分,即 for x in y 中的 x。参考下面的例子:

let v = vec!['a', 'b', 'c'];
for (index, value) in v.iter().enumerate() { println!("{} is at index {}", value, index);}
复制代码

在上面的例子中我们在 for 循环中使用模式来解构,或者说分解一个元组作为 for 循环的一部分。我们使用 enumerate 方法将迭代器变为包含元组的的迭代器,元组中的元素为原迭代器中的值和其索引位置,如第一个元组为 (0, 'a'),其与模式 (index, value) 相匹配,这时 index 是 0 ,value 是 'a',并执行循环打印第一行输出。上面的例子最终将打印出类似下面的输出结果:

$ cargo run   Compiling patterns v0.1.0 (file:///projects/patterns)    Finished dev [unoptimized + debuginfo] target(s) in 0.52s     Running `target/debug/patterns`a is at index 0b is at index 1c is at index 2
复制代码

let 语句

此前,我们只明确的提到过在 match 和 if let 中使用了模式,但事实上,我们在其它地方也使用了模式,甚至包括 let 语句,参考下面的例子:

let x = 5;
复制代码

在本书中我们进行了上百次类似的赋值,虽然我们可能没意识到,不过这也是在使用模式!let 转为模式的样子:


let PATTERN = EXPRESSION;
复制代码

对于语句 let x = 5; ,在 PATTERN 的位置上是变量名 x,变量名不过是形式特别简单的模式。Rust 将表达式与模式比较,并为所有找到的变量赋值。譬如在 let x = 5; 中,模式 x 意思是 “将匹配到的值绑定到变量 x”。因为变量名 x 就是整个模式,它实际上等于 “将任何值绑定到变量 x”。为了进一步的理解 let 的模式匹配,参考下面使用 let 解构一个元组的例子:

let (x, y, z) = (1, 2, 3);
复制代码

在上面的例子中,我们将一个元组与模式匹配。Rust 将数据 (1, 2, 3) 与模式 (x, y, z) 进行匹配,并发现它们是相匹配的。因此,会将 1 绑定到 x,2 绑定到 y ,3 绑定到 z。你可以将元组模式看作是将三个变量模式组合在一起。

另外,如果模式中元素的数量与元组中元素的数量不同,则整个类型不匹配,并会产生编译时错误。参考下面的例子:

let (x, y) = (1, 2, 3);
复制代码

如果尝试进行编译,我们将得到类似下面的错误:

$ cargo run   Compiling patterns v0.1.0 (file:///projects/patterns)error[E0308]: mismatched types --> src/main.rs:2:9  |2 |     let (x, y) = (1, 2, 3);  |         ^^^^^^   --------- this expression has type `({integer}, {integer}, {integer})`  |         |  |         expected a tuple with 3 elements, found one with 2 elements  |  = note: expected tuple `({integer}, {integer}, {integer})`             found tuple `(_, _)`
error: aborting due to previous error
For more information about this error, try `rustc --explain E0308`.error: could not compile `patterns`
To learn more, run the command again with --verbose.
复制代码

如果我们希望忽略元组中一个或多个值,可以使用 _ 或 ..,在后面 “忽略模式中的值” 部分会详细介绍。如果是模式中有太多的变量,解决方法是删除模式中多余的变量,使得其与元组中元素数数量相等。

函数参数

函数参数也可以是模式。参考下面的例子:

fn foo(x: i32) {    // code goes here}
复制代码

参数 x 就是一个模式!和之间我们介绍 let 时类似,我们也可以将元组与函数参数中的模式匹配。参考下面的例子:

fn print_coordinates(&(x, y): &(i32, i32)) {    println!("Current location: ({}, {})", x, y);}
fn main() { let point = (3, 5); print_coordinates(&point);}
复制代码

运行上面的例子会打印出 Current location: (3, 5)。main 函数中的作为参数传递的 &(3, 5) 会匹配模式 &(x, y),因此得到 x 的值 3,y 的值 5。另外,闭包中的参数与函数参数一样也可以进行模式匹配。

至此,我们已经了解了在哪些地方可以使用模式,但是模式在不同的地方并工作的方式并不相同;在有些地方,模式必须是不可反驳的(irrefutable) ;在在另一些情况下,其可以是可反驳的(refutable)。下面我们将讨论这两个概念。

发布于: 2 小时前阅读数: 3
用户头像

关注

公众号"山 顽石"欢迎大家关注;) 2021.03.23 加入

IT老兵,关注Rust、Java、前端、架构、大数据、AI、算法等;平时喜欢美食、旅游、宠物、健身、篮球、乒乓球等。希望能和大家交流分享。

评论

发布
暂无评论
Rust从0到1-模式-使用场景