模式是 Rust 中特殊的语法,它用来匹配类型中的结构,无论类型是简单还是复杂。结合使用模式和 match 表达式以及其他结构可以提供更多对程序控制流的支配权。模式由如下一些内容组合而成:
- 字面量
- 解构的数组、枚举、结构体或者元组
- 变量
- 通配符
- 占位符
这些部分描述了我们要处理的数据的形状,接着可以用其匹配值来决定程序是否拥有正确的数据来运行特定部分的代码。
我们通过将一些值与模式相比较来使用它。如果模式匹配这些值,我们对值部分进行相应处理。
一、所有可能会用到模式的位置
模式出现在 Rust 的很多地方。你已经在不经意间使用了很多模式!本部分是一个所有有效模式位置的参考。
match 分支
一个模式常用的位置是 match 表达式的分支。在形式上 match 表达式 由 match 关键字、用于匹配的值和一个或多个分支构成,这些分支包含一个模式和在值匹配分支的模式时运行的表达式:match VALUE {
PATTERN => EXPRESSION,
PATTERN => EXPRESSION,
PATTERN => EXPRESSION,
}
match 表达式必须是 穷尽(exhaustive)
的,意为 match 表达式所有可能的值都必须被考虑到。一个确保覆盖每个可能值的方法是在最后一个分支使用捕获所有的模式 —— 比如,一个匹配任何值的名称永远也不会失败,因此可以覆盖所有匹配剩下的情况。
有一个特定的模式 _
可以匹配所有情况,不过它从不绑定任何变量。这在例如希望忽略任何未指定值的情况很有用。
if let 条件表达式
if let 可以对应一个可选的带有代码的 else 在 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 purpleas the background color
。
注意 if let 也可以像 match 分支那样引入覆盖变量: if let Ok(age) = age 引入了一个新的覆盖变量 age ,它包含 Ok 成员中的值。这意味着 if age > 30 条件需要位于这个代码块内部;不能将两个条件组合为 if let Ok(age) = age && age > 30 ,因为我们希望与 30 进行比较的被覆盖的 age 直到大括号开始的新作用域才是有效的。
if let 表达式的缺点在于其穷尽性没有为编译器所检查,而 match 表达式则检查了。如果去掉最后的 else 块而遗漏处理一些情况,编译器也不会警告这类可能的逻辑错误。
while let 条件循环
一个与 if let 结构类似的是 while let 条件循环,它允许只要模式匹配就一直进行 while 循环。fn main() {
let mut stack = Vec::new();
stack.push(1);
stack.push(2);
stack.push(3);
while let Some(top) = stack.pop() {
println!("{}", top);
}
}
这个例子会打印出 3、2 接着是 1。 pop 方法取出 vector 的最后一个元素并返回 Some(value)
。如果 vector 是空的,它返回 None 。 while 循环只要 pop 返回 Some 就会一直运行其块中的代码。一旦其返回 None , while 循环停止。我们可以使用 while let 来 弹出栈中的每一个元素。
for 循环
for 循环是 Rust 中最常见的循环结构,不过还没有讲到的是 for 可以获取一个模式。在 for 循环中,模式是 for 关键字直接跟随的值,正如 for x in y
中的x。
下面展示了如何使用 for 循环来解构,或拆开一个元组作为 for 循环的一部分:
fn main() {
let v = vec!['a', 'b', 'c'];
for (index, value) in v.iter().enumerate() {
println!("{} is at index {}", value, index);
}
}
这会打印出:$ 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 0
b is at index 1
c is at index 2
这里使用 enumerate
方法适配一个迭代器来产生一个值和其在迭代器中的索引,他们位于一个元组中。第一个 enumerate 调用会产生元组 (0, ‘a’) 。当这个值匹配模式 (index, value) , index 将会是 0 而 value 将会是 ‘a’,并打印出第一行输出。
let 语句
我们只明确的讨论过通过 match 和 if let 使用模式,不过事实上也在别地地方使用过模式,包括 let 语句。例如,考虑一下这个直白的 let 变量赋值:let x = 5;
let 语句更为正式的样子如下:let PATTERN = EXPRESSION;
像 let x = 5;
这样的语句中变量名位于 PATTERN 位置,变量名不过是形式特别朴素的模式。我们将表达式与模式比较,并为任何找到的名称赋值。所以例如 let x = 5;
的情 况, x 是一个模式代表 “将匹配到的值绑定到变量 x”。同时因为名称 x 是整个模式,这个模式实际上等于 “将任何值绑定到变量 x ,不管值是什么”。
为了更清楚的理解 let 的模式匹配方面的内容,考虑下面代码中使用 let 和模式解构一个元组:fn main() {
let (x, y, z) = (1, 2, 3);
}
这里将一个元组与模式匹配。Rust 会比较值 (1, 2, 3) 与模式 (x, y, z) 并发现此值匹配这个模式。在这个例子中,将会把 1 绑定到 x , 2 绑定到 y 并将 3 绑定到 z 。你可以将这个元组模式看作是将三个独立的变量模式结合在一起。
如果模式中元素的数量不匹配元组中元素的数量,则整个类型不匹配,并会得到一个编译时错误。fn main() {
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 `(_, _)`
For more information about this error, try `rustc --explain E0308`.
error: could not compile `patterns` due to previous error
如果希望忽略元组中一个或多个值,也可以使用 _ 或 .. ,如 “忽略模式中的值” 部分所 示。如果问题是模式中有太多的变量,则解决方法是通过去掉变量使得变量数与元组中元素数相等。
函数参数
函数参数也可以是模式。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)
。值 &(3, 5) 会匹配模式 &(x, y) ,如此 x 得到了值3,而 y 得到了值5。
闭包类似于函数,也可以在闭包参数中使用模式。
现在我们见过了很多使用模式的方式了,不过模式在每个使用它的地方并不以相同的方式工 作;在一些地方,模式必须是 irrefutable
的,意味着他们必须匹配所提供的任何值。在另一些情况,他们则可以是 refutable
的。接下来让我们讨论这个。
二、Refutability(可反驳性): 模式是否会匹配失效
模式有两种形式:refutable(可反驳的)
和 irrefutable(不可反驳的)
。能匹配任何传递的可能值的模式被称为是 不可反驳的(irrefutable)
。一个例子就是 let x = 5; 语句中的 x , 因为 x 可以匹配任何值所以不可能会失败。对某些可能的值进行匹配会失败的模式被称为是 可反驳的(refutable)
。一个这样的例子便是 if let Some(x) = a_value 表达式中的 Some(x) ;如果变量 a_value 中的值是 None 而不是 Some ,那么 Some(x) 模式不能匹配。
让我们看看一个尝试在 Rust 要求不可反驳模式的地方使用可反驳模式以及相反情况的例子。let Some(x) = some_option_value;
如果 some_option_value 的值是 None,其不会成功匹配模式 Some(x),表明这个模式是可反驳的。然而, 因为 let 对于 None 匹配不能产生任何任何合法的代码,所以 let 语句只能接受不可反驳模式。Rust 会在编译时抱怨我们尝试在要求不可反驳模式的地方使用可反驳模式:$ cargo run
Compiling patterns v0.1.0 (file:///projects/patterns)
error[E0005]: refutable pattern in local binding: `None` not covered
--> src/main.rs:3:9
|
3 | let Some(x) = some_option_value;
| ^^^^^^^ pattern `None` not covered
|
= note: `let` bindings require an "irrefutable pattern", like a `struct` or an `enum` with only one variant
= note: for more information, visit https://doc.rust-lang.org/book/ch18-02-refutability.html
= note: the matched value is of type `Option<i32>`
help: you might want to use `if let` to ignore the variant that isn't matched
|
3 | if let Some(x) = some_option_value { /* */ }
|
For more information about this error, try `rustc --explain E0005`.
error: could not compile `patterns` due to previous error
因为我们没有覆盖(也不可能覆盖!)到模式 Some(x) 的每一个可能的值, 所以 Rust 会合理地抗议。
为了修复在需要不可反驳模式的地方使用可反驳模式的情况,可以修改使用模式的代码:不同于使用 let,可以使用 if let。如此,如果模式不匹配,大括号中的代码将被忽略,其余代码保持有效。if let Some(x) = some_option_value {
println!("{}", x);
}
我们给了代码一个得以继续的出路!虽然我们没办法在避免产生错误的情况下使用不可反驳模式,但这段使用可反驳模式的代码是完全有效的。
let 语句、 函数参数和 for 循环只能接受不可反驳的模式,因为通过不匹配的值程序无法 进行有意义的工作。 if let 和 while let 表达式被限制为只能接受可反驳的模式,因为根据定义他们意在处理可能的失败 ———— 条件表达式的功能就是根据成功或失败执行不同的操作。
通常无需担心可反驳和不可反驳模式的区别,不过确实需要熟悉可反驳性的概念,这样当在错误信息中看到时就知道如何应对。遇到这些情况,根据代码行为的意图,需要修改模式或者使用模式的结构。
三、所有的模式语法
本节会统一列出所有在模式中有效的语法并且会阐述你为什么可能会希望使用其中的每一个。
匹配字面值
可以直接匹配字面值模式。如下代码给出了一些例子:fn main() {
let x = 1;
match x {
1 => println!("one"),
2 => println!("two"),
3 => println!("three"),
_ => println!("anything"),
}
}
这段代码会打印 one 因为 x 的值是 1。
匹配命名变量
命名变量是匹配任何值的不可反驳模式,这在之前已经使用过数次。然而当其用于 match 表达式时情况会有些复杂。因为 match 会开始一个新作用域, match 表达式中作为模式的一部分声明的变量会覆盖 match 结构之外的同名变量 ———— 与所有变量一样。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 = {:?}", x, y);
让我们看看当 match 语句运行的时候发生了什么。第一个匹配分支的模式并不匹配 x 中定义的值,所以继续。
第二个匹配分支中的模式引入了一个新变量 y ,它会匹配任何 Some 中的值。因为我们在 match 表达式的新作用域中,这是一个新变量,而不是开头声明为值 10 的那个 y 。这个新的 y 绑定会匹配任何 Some 中的值,在这里是 x 中的值。因此这个 y 绑定了 x 中 Some 内部的值。这个值是 5,所以这个分支的表达式将会执行并打印出 Matched, y = 5 。
如果 x 的值是 None 而不是 Some(5) ,头两个分支的模式不会匹配,所以会匹配下划线。 这个分支的模式中没有引入变量 x ,所以此时表达式中的 x 会是外部没有被覆盖的 x 。 在这个假想的例子中, match 将会打印 Default case, x = None 。
一旦 match 表达式执行完毕,其作用域也就结束了,同理内部 y 的作用域也结束了。最后 的 println! 会打印 at the end: x = Some(5), y = 10 。
为了创建能够比较外部 x 和 y 的值,而不引入覆盖变量的 match 表达式,我们需要相应的使用带有条件的匹配守卫(match guard)
。本部分的后面会讨论匹配守卫。
多个模式
在 match 表达式中,可以使用 |
语法匹配多个模式,它代表或(or)的意思。例如,如下代码将 x 的值与匹配分支向比较,第一个分支有或选项,意味着如果 x 的值匹配此分支的任一个值,它就会运行:let x = 1;
match x {
1 | 2 => println!("one or two"),
3 => println!("three"),
_ => println!("anything"),
}
上面的代码会打印 one or two
。
通过..=匹配值的范围
该..=语法允许我们以匹配值的包含范围。let x = 5;
match x {
1..=5 => println!("one through five"),
_ => println!("something else"),
}
如果 x 是 1、2、3、4 或 5,第一个分支就会匹配。这相比使用 |
运算符表达相同的意思 更为方便;相比1..=5,使用|
则不得不指定1|2|3|4|5
。相反指定范围就简短的多,特别是在希望匹配比如从 1 到 1000 的数字的时候!
范围只允许用于数字或 char 值,因为编译器会在编译时检查范围不为空。 char 和数字值是 Rust 唯一知道范围是否为空的类型。
如下是一个使用 char 类型值范围的例子:let x = 'c';
match x {
'a'..='j' => println!("early ASCII letter"),
'k'..='z' => println!("late ASCII letter"),
_ => println!("something else"),
}
Rust 知道 c 位于第一个模式的范围内,并会打印出 early ASCII letter
。
解构并分解值
也可以使用模式来解构结构体、枚举、元组和引用,以便使用这些值的不同部分。让我们来分别看一看。
解构结构体
下面展示带有两个字段 x 和 y 的结构体 Point ,可以通过带有模式的 let 语句 将其分解:struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 0, y: 7 };
let Point { x: a, y: b } = p;
assert_eq!(0, a);
assert_eq!(7, b);
}
这段代码创建了变量a 和b 来匹配变量p 中的x 和y 字段。
这个例子展示了模式中的变量名不必与结构体中的字段名一致,不过通常希望变量名与字段名一致以便于理解变量来自于哪些字段。因为变量名匹配字段名是常见的,同时因为 let Point { x: x, y: y } = p;
包含了很多重复,所以对于匹配结构体字段的模式存在简写:只需列出结构体字段的名称,则模式创建的变量会有相同的名称。下面展示了与之前有着相同行为的代码,不过 let 模式创建的变量为 x 和 y 而不是 a 和 b :struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 0, y: 7 };
let Point { x, y } = p;
assert_eq!(0, x);
assert_eq!(7, y);
}
这段代码创建了变量x 和y,与变量p 中的x 和y 相匹配。其结果是变量x 和y包含结构体 p 中的值。
也可以在部分结构体模式中使用字面值进行结构,而不是为所有的字段创建变量。这允许我们测试一些字段为特定值的同时创建其他字段的变量。
下面展示了一个 match 语句将 Point 值分成了三种情况:直接位于 x 轴上(此时 y=0 为真)、位于y 轴上(x=0)或其他的点:struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 0, y: 7 };
match p {
Point { x, y: 0 } => println!("On the x axis at {}", x),
Point { x: 0, y } => println!("On the y axis at {}", y),
Point { x, y } => println!("On neither axis: ({}, {})", x, y),
}
}
第一个分支通过指定字段 y 匹配字面值 0 来匹配任何位于 x 轴上的点。此模式仍然创建 了变量 x 以便在分支的代码中使用。类似的,第二个分支通过指定字段 x 匹配字面值 0 来匹配任何位于 y 轴上的点,并为字段 y 创建了变量 y 。第三个分支没有指定任何字面值,所以其会匹配任何其他的 Point 并为 x 和 y 两个字段创建变量。
在这个例子中,值 p 因为其 x 包含 0 而匹配第二个分支,因此会打印出 On the y axis at 7
。
解构枚举
编写一个 match 使用模式解构每一个内部值enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
fn main() {
let msg = Message::ChangeColor(0, 160, 255);
match msg {
Message::Quit => {
println!("The Quit variant has no data to destructure.")
}
Message::Move { x, y } => {
println!(
"Move in the x direction {} and in the y direction {}",
x, y
);
}
Message::Write(text) => println!("Text message: {}", text),
Message::ChangeColor(r, g, b) => println!(
"Change the color to red {}, green {}, and blue {}",
r, g, b
),
}
}
这段代码会打印出 Change the color to red 0, green 160, and blue 255
。尝试改变 msg 的值来观察其他分支代码的运行。
对于像 Message::Quit 这样没有任何数据的枚举成员,不能进一步解构其值。只能匹配其字面值 Message::Quit ,因此模式中没有任何变量。
对于像 Message::Move 这样的类结构体枚举成员,可以采用类似于匹配结构体的模式。在成员名称后,使用大括号并列出字段变量以便将其分解以供此分支的代码使用。
对于像 Message::Write 这样的包含一个元素,以及像 Message::ChangeColor 这样包含两个元素的类元组枚举成员,其模式则类似于用于解构元组的模式。模式中变量的数量必须与成员中元素的数量一致。
解构嵌套结构体和枚举
到目前为止,我们所有的例子都是匹配一层深度的结构体或枚举。匹配也适用于嵌套项目!enum Color {
Rgb(i32, i32, i32),
Hsv(i32, i32, i32),
}
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(Color),
}
fn main() {
let msg = Message::ChangeColor(Color::Hsv(0, 160, 255));
match msg {
Message::ChangeColor(Color::Rgb(r, g, b)) => println!(
"Change the color to red {}, green {}, and blue {}",
r, g, b
),
Message::ChangeColor(Color::Hsv(h, s, v)) => println!(
"Change the color to hue {}, saturation {}, and value {}",
h, s, v
),
_ => (),
}
}
match表达式中第一个Message::ChangeColor分支的模式匹配包含Color::Rgb变体的枚举变体;然后模式绑定到三个内部i32值。第二个臂的模式也匹配Message::ChangeColor枚举变体,但内部枚举匹配Color::Hsv变体。match即使涉及两个枚举,我们也可以在一个表达式中指定这些复杂的条件。
解构结构体和元组
甚至可以用复杂的方式来合成、匹配和嵌套解构模式。如下是一个负责结构体的例子,其中结构体和元组嵌套在元组中,并将所有的原始类型解构出来:let ((feet, inches), Point {x, y}) = ((3, 10), Point { x: 3, y: -10 });
这将复杂的类型分解成部分组件以便可以单独使用我们感兴趣的值。
通过模式解构是一个方便利用部分值片段的手段,比如结构体中每个单独字段的值。
忽略模式中的值
有时忽略模式中的一些值是有用的,比如 match 中最后捕获全部情况的分支实际上没有做任何事,但是它确实对所有剩余情况负责。有一些简单的方法可以忽略模式中全部或部分值: 使用 _
模式(我们已经见过了),在另一个模式中使用 _
模式,使用一个以下划线开始的名称,或者使用 ..
忽略所剩部分的值。让我们来分别探索如何以及为什么要这么做。
使用 _ 忽略整个值
我们已经使用过下划线作为匹配但不绑定任何值的通配符模式了。虽然下划线模式作为 match 表达式最后的分支特别有用,也可以将其用于任意模式,包括函数参数中fn foo(_: i32, y: i32) {
println!("This code only uses the y parameter: {}", y);
}
fn main() {
foo(3, 4);
}
这段代码会完全忽略作为第一个参数传递的值,3,并会打印出 This code only uses the y parameter: 4
。大部分情况当你不再需要特定函数参数时,最好修改签名不再包含无用的参数。
在一些情况下忽略函数参数会变得特别有用,比如实现 trait
时,当你需要特定类型签名但是函数实现并不需要某个参数时。此时编译器就不会警告说存在未使用的函数参数,就跟使用命名参数一样。
使用嵌套的 _ 忽略部分值
当只需要测试部分值但在期望运行的代码部分中没有使用它们时,也可以在另一个模式内部使用 _
来只忽略部分值。下面展示了负责从设置中获取一个值的代码。业务需求是用户不允许覆盖某个设置中已经存在的自定义配置,但是可以重设设置和在目前未设置时提供新的设置。fn main() {
let mut setting_value = Some(5);
let new_setting_value = Some(10);
match (setting_value, new_setting_value) {
(Some(_), Some(_)) => {
println!("Can't overwrite an existing customized value");
}
_ => {
setting_value = new_setting_value;
}
}
println!("setting is {:?}", setting_value);
}
这段代码会打印出 Can't overwrite an existing customized value
接着是 setting is Some(5)
。在第一个匹配分支,我们不需要匹配或使用任一个 Some 成员中的值;重要的部分是需要测试 setting_value 和 new_setting_value 都为 Some 成员的情况。在这种情况,我们希望打印出为何不改变 setting_value ,并且不会改变它。
对于所有其他情况( settingvalue 或 new_setting_value 任一为 None ),这由第二个分支的 `` 模式体现,这时确实希望允许 new_setting_value 变为 setting_value 。
也可以在一个模式中的多处使用下划线来忽略特定值,如下面所示,这里忽略了一个 五元元组中的第二和第四个值:fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(first, _, third, _, fifth) => {
println!("Some numbers: {}, {}, {}", first, third, fifth)
}
}
}
这会打印出 Some numbers: 2, 8, 32 , 值 4 和 16 会被忽略。
通过在名字前以一个下划线开头来忽略未使用的变量
如果你创建了一个变量却不在任何地方使用它, Rust 通常会给你一个警告,因为这可能会是个 bug。但是有时创建一个还未使用的变量是有用的,比如你正在设计原型或刚刚开始一个项目。这时你希望告诉 Rust 不要警告未使用的变量,为此可以用下划线作为变量名的开头。
创建了两个未使用变量,不过当运行代码时只会得到其中一个的警告:fn main() {
let _x = 5;
let y = 10;
}
这里得到了警告说未使用变量 y ,不过没有警告说未使用下划线开头的变量。
注意, 只使用 _
和使用以下划线开头的名称有些微妙的不同:比如 _x
仍会将值绑定到变量,而 则完全不会绑定。为了展示这个区别的意义,下面会产生一个错误。fn main() {
let s = Some(String::from("Hello!"));
if let Some(_s) = s {
println!("found a string");
}
println!("{:?}", s);
}
我们会得到一个错误,因为 s 的值仍然会移动进 _s
,并阻止我们再次使用 s 。然而只使用下划线本身,并不会绑定值。下面能够无错编译,因为 s 没有被移动进 :fn main() {
let s = Some(String::from("Hello!"));
if let Some(_) = s {
println!("found a string");
}
println!("{:?}", s);
}
上面的代码能很好的运行;因为没有把 s 绑定到任何变量,它没有被移动。
用 .. 忽略剩余值
对于有多个部分的值,可以使用 ..
语法来只使用部分并忽略其它值,同时避免不得不每一个忽略值列出下划线。 ..
模式会忽略模式中剩余的任何没有显式匹配的值部分。下面代码中,有一个 Point 结构体存放了三维空间中的坐标。在 match 表达式中,我们希望只操作x 坐标并忽略y 和z 字段的值:struct Point {
x: i32,
y: i32,
z: i32,
}
let origin = Point { x: 0, y: 0, z: 0 };
match origin {
Point { x, .. } => println!("x is {}", x),
}
这里列出了 x 值,接着仅仅包含了 ..
模式。这比不得不列出 y: _
和 z: _
要来得简单,特别是在处理有很多字段的结构体,但只涉及一到两个字段时的情形。..
会扩展为所需要的值的数量。下面代码中扩展了元组的使用:fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(first, .., last) => {
println!("Some numbers: {}, {}", first, last);
}
}
}
这里用 first 和 last 来匹配第一个和最后一个值。 ..
将匹配并忽略中间的所有值。
然而使用 ..
必须是无歧义的。如果期望匹配和忽略的值是不明确的,Rust 会报错。fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(.., second, ..) => {
println!("Some numbers: {}", second)
},
}
}
如果编译上面的例子,会得到下面的错误:$ cargo run
Compiling patterns v0.1.0 (file:///projects/patterns)
error: `..` can only be used once per tuple pattern
--> src/main.rs:5:22
|
5 | (.., second, ..) => {
| -- ^^ can only be used once per tuple pattern
| |
| previously used here
error: could not compile `patterns` due to previous error
Rust 不可能决定在元组中匹配 second 值之前应该忽略多少个值,以及在之后忽略多少个值。这段代码可能表明我们意在忽略 2,绑定 second 为 4,接着忽略 8、16 和 32;抑或是意在忽略 2 和 4,绑定 second 为 8,接着忽略 16 和 32,以此类推。变量名 second 对于 Rust 来说并没有任何特殊意义,所以会得到编译错误,因为在这两个地方使用 ..
是有歧义的。
匹配守卫提供的额外条件
匹配守卫(match guard)是一个指定与 match 分支模式之后的额外 if 条件,它也必须被满足才能选择此分支。匹配守卫用于表达比单独的模式所能允许的更为复杂的情况。
这个条件可以使用模式中创建的变量。下面的代码展示了一个 match ,其中第一个分支有模 式 Some(x) 还有匹配守卫 if x < 5
:let num = Some(4);
match num {
Some(x) if x < 5 => println!("less than five: {}", x),
Some(x) => println!("{}", x),
None => (),
}
上例会打印出 less than five: 4
。当 num 与模式中第一个分支比较时,因为 Some(4) 匹配 Some(x) 所以可以匹配。接着匹配守卫检查 x 值是否小于 5,因为 4 小于 5,所以第一 个分支被选择。
相反如果 num 为 Some(10) ,因为 10 不小于 5 所以第一个分支的匹配守卫为假。接着 Rust 会前往第二个分支,这会匹配因为它没有匹配守卫所以会匹配任何 Some 成员。
无法在模式中表达 if x < 5
的条件,所以匹配守卫提供了表现此逻辑的能力。
新变量意味着不能够测试外部变量的值。下面代码展示了如何使用匹配守卫修复这个问题:fn main() {
let x = Some(5);
let y = 10;
match x {
Some(50) => println!("Got 50"),
Some(n) if n == y => println!("Matched, n = {}", n),
_ => println!("Default case, x = {:?}", x),
}
println!("at the end: x = {:?}, y = {}", x, y);
}
现在这会打印出 Default case, x = Some(5)
。现在第二个匹配分支中的模式不会引入一个覆盖外部 y 的新变量 y ,这意味着可以在匹配守卫中使用外部的 y 。相比指定会覆盖外部y 的模式 Some(y) ,这里指定为 Some(n) 。此新建的变量 n 并没有覆盖任何值,因为 match 外部没有变量 n 。
在匹配守卫 if n == y
中,这并不是一个模式所以没有引入新变量。这个 y 正是 外部的 y 而不是新的覆盖变量 y ,这样就可以通过比较 n 和 y 来表达寻找一个与外部 y 相同的值的概念了。
也可以在匹配守卫中使用或运算符 |
来指定多个模式,同时匹配守卫的条件会作用域所有的模式。下面代码展示了结合匹配守卫与使用了 |
的模式的优先级。这个例子中重要的部分是匹配守卫 if y 作用于 4、5 和 6,即使这看起来好像 if y 只作用于 6:let x = 4;
let y = false;
match x {
4 | 5 | 6 if y => println!("yes"),
_ => println!("no"),
}
这个匹配条件表明此分支值匹配 x 值为 4、5 或 6 同时 y 为 true 的情况。运行这段代码时会发生的是第一个分支的模式因 x 为 4 而匹配,不过匹配守卫 if y 为假,所以第一个分支不会被选择。代码移动到第二个分支,这会匹配,此程序会打印出 no 。
这是因为 if 条件作用于整个 4 | 5 | 6 模式,而不仅是最后的值 6 。换句话说,匹配守卫与模式的优先级关系看起来像这样:(4 | 5 | 6) if y => ...
而不是:4 | 5 | (6 if y) => ...
可以通过运行代码时的情况看出这一点:如果匹配守卫只作用于由 |
运算符指定的值列表的最后一个值,这个分支就会匹配且程序会打印出 yes 。
@ 绑定
at 运算符 @
允许我们在创建一个存放值的变量的同时测试其值是否匹配模式。下面代码展示了一个例子,这里我们希望测试 Message::Hello 的 id 字段是否位于 3..=7
范围内,同时也希望能其值绑定到 id_variable 变量中以便此分支相关联的代码可以使用它。可以将id_variable 命名为 id ,与字段同名,不过出于示例的目的这里选择了不同的名称:enum Message {
Hello { id: i32 },
}
let msg = Message::Hello { id: 5 };
match msg {
Message::Hello {
id: id_variable @ 3..=7,
} => println!("Found an id in range: {}", id_variable),
Message::Hello { id: 10..=12 } => {
println!("Found an id in another range")
}
Message::Hello { id } => println!("Found some other id: {}", id),
}
上例会打印出 Found an id in range: 5
。通过在 3..=7
之前指定 id_variable @ ,我们捕获了任何匹配此范围的值并同时测试其值匹配这个范围模式。
第二个分支只在模式中指定了一个范围,分支相关代码代码没有一个包含 id 字段实际值的变量。 id 字段的值将会是 10、11 或 12,不过这个模式的代码并不知情也不能使用 id 字 段中的值,因为没有将 id 值保存进一个变量。
最后一个分支指定了一个没有范围的变量,此时确实拥有可以用于分支代码的变量 id ,因为这里使用了结构体字段简写语法。不过此分支中不能像头两个分支那样对 id 字段的值进行任何测试:任何值都会匹配此分支。
使用 @
可以在一个模式中同时测试和保存变量值。