模式(patterns)是Rust中一种用来匹配类型结构的特殊语法。将模式与match表达式或其他工具配合使用可以更好地控制程序流程。
一个模式匹配通常由以下组件构成:
- 字面量
- 解构地数组、枚举、结构体或元组
- 变量
- 通配符
- 占位符
模式被用来与某个特定地值进行匹配。如果模式与值匹配成功,那么我们就可以在代码中使用这个值的某些部分。
一、所有可以使用模式的场合
1、match分支
模式可以被应用在match表达式的分支中。match表达式在形式上由macth关键、 待匹配的值,以及至少一个匹配分支组合而成,而分支则由一个模式及匹配成功后应当执行的表达式组成:
match 值
{
模式 => 表达式,
模式 => 表达式,
模式 => 表达式,
}
match表达式必须穷尽(exhaustive)匹配值的所有可能性。为了确保代码满足这一要求,我们可以在最后的分支处使用全匹配模式。
另外,还有一个特殊的_
模式可以用来匹配所有可能的值,且不将它们绑定到任何一个变量上,因此,这个模式常常被用作匹配列表中的最后一个分支。当你想要忽略所有未被指定的值时,_
模式会非常有用。
2、if let条件表达式
我们之间将if let
表达式只匹配单个分支的match表达式来使用。但实际上if let
还能够添加一个可选的else分支,如果if let
对应的模式没有匹配成功,那么else分支的代码就会得到执行。另外,我们还可以混合使用if let
、else if
及else if let
表达式来进行匹配。
//示例18-1:混合使用if let、else if、else if let和else
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!("Thuesday 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");
}
}
和match分支类似,if let
分支能够以同样的方式对变量进行覆盖。if let Ok(age) = age
这条语句中引入了信的变量age
来存储Ok
变体中的值,而它覆盖了右侧的同名变量。这意味着我们必须把判断条件if age > 30
放置到匹配成功后执行的代码块中,而不能把这两个条件组合成if let Ok(age) = age && age > 30
。因为覆盖了同名变量的age只有在花括号的新作用域中才会变得有效。
与match表达式不同,if let
表达式的不利之处在于它不会强制开发者穷尽值的所有可能。即便我们省略了随后可选的else块,并遗漏了某些需要处理的情形,编译器也不会在这里警告我们存在可能的逻辑性缺陷。
3、While let条件循环
条件循环while let
与if let
十分相似,但它会反复执行同一个模式匹配直到出现失败的情形。如下示例演示了一个动态数组作为栈并以先进后出的方式打印相关值:
//示例18-2:只有stack.pop()返回的值是Some变体,那么While let循环就会不断地进行打印
fn main() {
let mut stack = Vec::new();
stack.push(1);
stack.push(2);
stack.push(3);
while let Some(top) = stack.pop() {
println!("{}", top);
}
}
其中的pop方法会试图取出动态数组的最后一个元素并将它包裹在Some(value)
中返回。如果动态数组为空,则pop返回None。While循环会在pop返回Some时迭代执行循环体中的代码,并在pop返回None时结束循环。使用While便可以将栈中的每个元素逐一弹出了。
4、for循环
for循环是Rust代码中最为常用的循环结构,而同样可以在for循环内使用模式。for语句中紧随关键字for的值就是一个模式。
//示例18-3:在for循环中使用模式来结构元组
fn main() {
let v = vec!['a','b','c'];
for (index, value) in v.iter().enumerate() {
println!{"{} is at index {}", value, index};
}
}
上面的代码使用了enumerate方法来作为迭代的适配器,它会在每次迭代过程中生成一个包含值本身及值索引的元组。例如,首次调用enumerate
会产生元组(0, 'a')
。当我们将这个值与模式(index, value)
进行匹配时,index就会被赋值为0,而value则会被赋值为'a'
,这就是第一行输出的内容。
5、let语句
正式的let语句的定义如下:
let PATTERN = EXPRESSION;
在类似于let x = 5;
的语句中,单独的变量名作为最朴素的模式被放于PATTERN对应的位置。Rust会将表达式与模式进行比较,并为所有找到的名称赋值。因此,在let x = 5;
这个示例中,x作为模式表达的含义是”将此处匹配到的所有内容绑定至变量x“。因此x就是整个模式本身,所以它实际上意味着”无论表达式会返回什么样的值,我们都可以将它绑定至变量x中“。
为了更清晰地理解let语句中地模式匹配,我们在如下示例中演示了一条使用let模式来解构元组的语句:
//示例18-4:使用模式来解构元组并一次性创建出3个变量
fn main() {
let (x, y, z) = (1,2,3);
}
我们这个示例中使用模式来匹配一个元组。由于Rust在比较值(1, 2, 3)
与模式(x, y, z)
时发现它们是一一对应的,所有Rust会最终将(1, 2, 3)
分别绑定至(x, y, z)
上。
如果模式中元素的数量与元组中的数量不同,那么整个类型就会匹配失败,尽而导致编译出错。
//示例18-5:一个错误的模式,其中变量的数量与元组中元素的数量不匹配
fn main() {
let (x, y) = (1,2,3);
}
6、函数的参数
函数的参数同样也是模式。如下示例声明了一个名为foo的函数,它接收一个名为x的i32类型参数。
//示例18-6:在参数中使用了模式的函数签名
fn foo(x: i32) {
//在此编写函数代码
}
签名中的x部分就是一个模式!与let语言类似,我们同样可以在函数参数中使用模式去匹配元组。
//示例18-7:在参数中解构元组的函数
fn print_coordinates(&(x, y): &(i32, i32)) {
println!("Current location: ({}, {})", x, y);
}
fn main() {
let point = (3, 5);
print_coordinates(&point);
}
由于模式&(x, y)
能够和值&(3, 5)
匹配,所以x的值为3,y的值为5。
二、可失败性:模式是否会匹配失败
模式可分为不可失败(irrefutable)和可失败(refutable)两种类型。不可失败的模式能够匹配任何传入的值。可失败模式则可能因为某些特定的值而匹配失败。
函数参数、let语句及for循环只接收不可失败模式,因为在这些场合下,我们的程序无法在值不匹配时执行任何有意义的行为。if let
和while let
表达式则只接收可失败模式,因为它们在被设计时就将匹配失败的情形考虑在内了:条件表达式的功能就是根据条件的成功与否执行不同的操作。
我们在不可失败模式的场合中使用失败模式会发生编译错误。如下示例的let语句使用了一个可失败的Some(x)
模式。
//示例18-8:试图在let中使用一个可失败的模式
fn main() {
let some_option_value: Option<i32> = None;
let Some(x) = some_option_value;
}
如果some_option_value
的值是None,那么我们就无法成功地匹配模式Some(x)
,这也意味着这个模式本身是可失败的。然后,let语句只能接收一个不可失败模式,因为这段代码无法通过None值执行任何有效的操作。
因为模式Some(x)
无法覆盖表达式右侧值的所有可能的情形,所以Rust产生了一个合理的编译报错。
为了修复上述示例的问题,我们可以使用if let
来代替涉及模式的那一部分let代码。新的代码能够在我们遇到模式不酦醅的时候跳过花括号中的代码块,并给予程序一个合法的方式继续运行。
//示例18-9:将let替换为支持可失败模式的if let及对应的代码块
fn main() {
let some_option_value: Option<i32> = None;
if let Some(x) = some_option_value {
println!("{}", x);
}
}
上面的方式给代码添加了一个合法的出口,这样就可以顺利地运行这段代码,尽管这意味着我们必须在此时使用可失败模式。
假如我们在if let
中使用了一个不可失败模式,那么这段代码是无法通过编译的,如下所示:
//示例18-10:尝试在if let表达式中使用一个不可失败模式
fn main() {
if let x = 5 {
println!("{}", x);
};
}
因此,在match表达式的匹配分支中,除了最后一个,其他必须全部使用可失败模式,而最后的分支则应该使用不可失败模式,因为它需要匹配值的所有剩余的情形。
Rust允许你在仅有一个分支的match表达式中使用不可失败模式,但这种语法几乎没有任何用户,它可以被简单的let语句所代替。
三、模式语法
1、匹配字面量
我们可以直接使用模式来匹配字面量,如下所示:
fn main() {
let x = 1;
match x {
1 => println!("one"),
2 => println!("two"),
3 => println!("three"),
_ => println!("anything"),
}
}
2、匹配命名变量
命名变量(named variable)是一种可以匹配任何值的不可失败模式。当我们在match表达式中使用命名变量时,由于match开启了一个新的作用域,所有被定义在match表达式内作为模式一部分的变量会覆盖掉match结构外的同名变量。
如下示例,我们声明的变量x与y分别存储了Some(5)
和10
。接着我们编写了一个match表达式来匹配x的值。
//示例18-11:match表达式的一个分支引入了一个覆盖变量y
fn main() {
let x = Some(5);
let y = 10;
match x {
Some(50) => println!("Got 50"),
Some(y) => println!("Matched, y = {y}"),
_ => println!("Default case, x = {:?}", x),
}
println!("at the end: x = {:?}, y = {y}", x);
}
第一个匹配分支的模式与x中的值不匹配,所以我们简单地跳过该分支即可。
第二个匹配分支的模式引入了新的变量y,它会匹配Some变体中携带的任意值。因为我们处在match表达式创建的新作用域中,所以我们这里的y是一个新的变量,而不是我们在程序起始处声明的那个存储了10的y。这个新的y的绑定能够匹配Some中的人一直,而x正式一个Some。因此,新的y会被绑定到x变量中Some内部的值。由于这个值是5,所以当前分支的表达式会在执行后打印出Matched, y = 5
。
如果x不是Some(5)而是None,那么它会在前两个分支的模式匹配中匹配失败,进而与最后的那个下划线模式相匹配。由于我们没有在下划线模式的分支内引入x变量,所以这个表达式使用的x没有被任何变量所覆盖,它依然是外部作用域中的x。
match表达式创建出来的作用域会随着当前表达式的结束而结束,而它内部的y自然不能幸免,最后打印出at the end: x = Some(5), y = 10
。