模式时Rust
中特殊的语法,它用来匹配类型中的结构,无论类型是简单还是复杂。结合使用模式和 match
表达式以及其他结构可以提供更多对程序控制流的支配权。模式由如下一些内容组合而成:
- 字面量
- 解构的数组、枚举、结构体或者元组
- 变量
- 通配符
- 占位符
这些部分描述了我们要处理的数据的形状,接着可以用其匹配值来决定程序是否拥有正确的数据来运行特定部分的代码。
我们通过将一些值与模式相比较来使用它。如果模式匹配这些值,我们对值部分进行相应处理。match
表达式时像硬币分类器那样使用模式。如果数据符合这个形状,就可以使用这些命名的片段。如果不符合,与该模式相关的代码则不会运行。
所有可能会用到模式的位置
match分支
- 一个模式常用的位置是
match
表达式的分支。在形式上match
表达式由match
关键字、用于匹配的值和一个或多个分支构成,这些分支包含一个模式和在值匹配分支的模式时运行的表达式:
match VALUE {
PATTERN => EXPRESSION,
PATTERN => EXPRESSION,
PATTERN => EXPRESSION,
}
match
表达式必须是 穷尽(exhaustive)的,意为match
表达式所有可能的值都必须被考虑到。一个确保覆盖每个可能值的方法是在最后一个分支使用捕获所有的模式:比如,一个匹配任何值的名称永远也不会失败,因此可以覆盖所有匹配剩下的情况。- 有一个特定的模式
_
可以匹配所有情况,不过它从不绑定任何变量。这在例如希望忽略任何未指定值的情况很有用。
if let条件表达式
- 之前讨论过
if let
表达式,以及如何等同于只编写一个match语句简写的。if let
可以对应一个可选带有代码的else
在if let
中模式不匹配时运行。 - 可以组合并匹配
if let
、else if
和else if let
表达式。这相比match
表达式一次只能将一个值与模式比较提供了更多灵活性;一系列if let
、else if
、else if let
分支并不要求其条件相互关联。 - 示例中展示了一系列针对不同条件的检查来决定背景颜色应该是什么。为了达到这个例子的目的,我们创建了硬编码值的变量,在真实程序中则可能由询问用户获得。
- src/main.rs
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
分支那样引入覆盖变量: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
循环。示例展示了一个使用while let
的例子,它使用vector
作为栈并以先进后出的方式打印出vector
中的值:
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);
}
- 实例中代码打印出:
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
变量赋值:
fn main() {
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。你可以将这个元组模式看作是将三个独立的变量模式结合在一起。 - 如果模式中元素的数量不匹配元组中元素的数量,则整个类型不匹配,并会得到一个编译时错误。例如,示例 18-5 展示了尝试用两个变量解构三个元素的元组,这是不行的:
let (x, y) = (1, 2, 3);
- 尝试编译会得到下面的错误:
error[E0308]: mismatched types
--> src/main.rs:2:9
|
2 | let (x, y) = (1, 2, 3);
| ^^^^^^ expected a tuple with 3 elements, found one with 2 elements
|
= note: expected type `({integer}, {integer}, {integer})`
found type `(_, _)`
函数参数
- 函数参数也可以是模式。列表中的代码声明了一个叫做
foo
的函数,它获取一个i32
类型的参数x
,现在这看起来应该很熟悉:
fn main() {
fn foo(x: i32) {
// 代码
}}
x
部分就是一个模式!类似于之前对let
所做的,可以在函数参数中匹配元组。列表将传递给函数的元组拆分为值:- src/main.rs
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
。 - 由于闭包和函数相似,所以闭包也可以使用模式。
Refutability (可反驳性) :模式是否会匹配失效
- 模式有两种形式:
refutable
(可反驳的)和irrefutable
(不可反驳的)。能匹配任何传递的可能值的模式被称为是 不可反驳的(irrefutable)。一个例子就是let x = 5;
语句中的 x,因为 x 可以匹配任何值所以不可能会失败。对某些可能的值进行匹配会失败的模式被称为是 可反驳的(refutable)。一个这样的例子便是if let Some(x) = a_value
表达式中的Some(x)
;如果变量 **a_value **中的值是 None 而不是 Some,那么 Some(x) 模式不能匹配。 - 函数参数、
let
语句和for
循环只能接受不可反驳的模式,因为通过不匹配的值程序无法进行有意义的工作。if let
和while let
表达式被限制为只能接受可反驳的模式,因为根据定义他们意在处理可能的失败:条件表达式的功能就是根据成功或失败执行不同的操作。 - 通常我们无需担心可反驳和不可反驳模式的区别,不过确实需要熟悉可反驳性的概念,这样当在错误信息中看到时就知道如何应对。遇到这些情况,根据代码行为的意图,需要修改模式或者使用模式的结构。
- 让我们看看一个尝试在 Rust 要求不可反驳模式的地方使用可反驳模式以及相反情况的例子。在示例中,有一个
let
语句,不过模式被指定为可反驳模式Some(x)
。如你所见,这不能编译:
let Some(x) = some_option_value;
- 若
some_option_value
的值是None
,则不会成功匹配模式Some(x)
,表明这个模式是可反驳的。然而let
语句只能接受不可反驳模式,因为代码不能通过None
值进行有效的操作。Rust 会在编译时抱怨我们尝试在要求不可反驳模式的地方使用可反驳模式:
error[E0005]: refutable pattern in local binding: `None` not covered
-->
|
3 | let Some(x) = some_option_value;
| ^^^^^^^ pattern `None` not covered
- 因为我们没有覆盖(也无法覆盖!)模式
Some(x)
的每一个可能的有效值,Rust
理所当然地产生了编译器错误。 - 为了修复在需要不可反驳模式的地方使用可反驳模式的情况,可以修改使用模式的代码:不同于使用
let
,可以使用if let
。如此,如果模式不匹配,大括号中的代码将被忽略,其余代码保持有效。
fn main() {
let some_option_value: Option<i32> = None;
if let Some(x) = some_option_value {
println!("{}", x);
}
}
- 我们给了代码一个得以继续的出路!这段代码完全有效,尽管这意味着我们不能在避免产生错误的情况下使用无可辩驳的模式。如果为
if let
提供了一个总是会匹配的模式,比如示例中的x
,编译器会给出一个警告。
if let x = 5 {
println!("{}", x);
};
- 编译器会提示把不可反驳模式应用于if let 没有意义:
warning: irrefutable if-let pattern
--> <anon>:2:5
|
2 | / if let x = 5 {
3 | | println!("{}", x);
4 | | };
| |_^
|
= note: #[warn(irrefutable_let_patterns)] on by default
- 基于此,match 匹配分支必须使用可反驳模式,除了最后一个分支需要使用能匹配任何剩余值的不可反驳模式。Rust 允许我们在只有一个匹配分支的
match
中使用不可反驳模式,不过这么做不是特别有用,并可以被更简单的let
语句替代。
模式语法
匹配字面量
- 可以直接匹配字面量模式。如下代码给出了一些例子:
let x = 1;
match x {
1 => println!("one"),
2 => println!("two"),
3 => println!("three"),
_ => println!("anything"),
}
- 这段代码打印出
one
因为x
的值是1
.如果希望代码获得特定的具体值,则该语法很有用。
匹配命名变量
- 命名变量是匹配任何值的不可反驳模式,这在之前已经使用过数次。然而当其用于
match
表达式时情况会有些复杂。因为match
会开始一个新作用域,match
表达式中作为模式的一部分声明的变量会覆盖match
结构之外的同名变量,与所有变量一样。在示例中,声明了一个值为Some(5)
的变量x
和一个值为10
的变量y
。接着在值x
上创建了一个match
表达式。观察匹配分支中的模式和结尾的println!
,并在运行此代码或进一步阅读之前推断这段代码会打印什么。
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 = {:?}", 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
。
解构枚举
- 本书之前的部分曾经解构过枚举,比如解构了一个
Option<i32>
。一个当时没有明确提到的细节是解构枚举的模式需要对应枚举所定义的储存数据的方式。让我们以示例中的Message
枚举为例,编写一个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
这样包含三个元素的类元组枚举成员,其模式则类似于用于解构元组的模式。模式中变量的数量必须与成员中元素的数量一致。
解构嵌套的结构体和枚举
- 目前为止,所有的例子都只匹配了深度为一级的结构体或枚举。当然也可以匹配嵌套的项!
- 我们可以重构列表的代码来同时支持
RGB
和HSV
色彩模式:
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
表达式第一个分支的模式匹配一个包含Color::Rgb
枚举成员的Message::ChangeColor
枚举成员,然后模式绑定了3
个内部的i32
值。第二个分支的模式也匹配一个Message::ChangeColor
枚举成员, 但是其内部的枚举会匹配Color::Hsv
枚举成员。我们可以在一个match
表达式中指定这些复杂条件,即使会涉及到两个枚举。
解构结构体和元组
- 甚至可以用复杂的方式来混合、匹配和嵌套解构模式。如下是一个复杂结构体的例子,其中结构体和元组嵌套在元组中,并将所有的原始类型解构出来:
fn main() {
struct Point {
x: i32,
y: i32,
}
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
,并且不会改变它。 - 对于所有其他情况(
setting_value
或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
仍会将值绑定到变量,而_
则完全不会绑定。为了展示这个区别的意义,
let s = Some(String::from("Hello!"));
if let Some(_s) = s {
println!("found a string");
}
println!("{:?}", 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
字段的值:
fn main() {
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)
},
}
}
- 如果想要编译上面的例子,会得出下面的错误:
error: `..` can only be used once per tuple or tuple struct pattern
--> src/main.rs:5:22
|
5 | (.., second, ..) => {
| ^^
- 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
:
fn main() {
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
的条件,所以匹配守卫提供了表现此逻辑的能力。 - 在示例中,我们提到可以使用匹配守卫来解决模式中变量覆盖的问题,那里
match
表达式的模式中新建了一个变量而不是使用match
之外的同名变量。新变量意味着不能够测试外部变量的值。示例展示了如何使用匹配守卫修复这个问题。
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
:
fn main() {
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
,与字段同名,不过出于示例的目的这里选择了不同的名称。
fn main() {
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
字段的值进行测试:任何值都会匹配此分支。 - 使用
@
可以在一个模式中同时测试和保存变量值。
总结
- 模式是
Rust
中一个很有用的功能,它帮助我们区分不同类型的数据。当用于match
语句时,Rust
确保模式会包含每一个可能的值,否则程序将不能编译。let
语句和函数参数的模式使得这些结构更强大,可以在将值解构为更小部分的同时为变量赋值。可以创建简单或复杂的模式来满足我们的要求。