【Rust自学】18.3. 模式(匹配)的语法
喜欢的话别忘了点赞、收藏加关注哦(加关注即可阅读全文),对接下来的教程有兴趣的可以关注专栏。谢谢喵!(=・ω・=)
18.3.1. 匹配字面值
模式可以直接匹配字面值。看个例子:
let x = 1;
match x {
1 => println!("one"),
2 => println!("two"),
3 => println!("three"),
_ => println!("anything"),
}
此代码打印one
,因为x
中的值为 1。当希望代码在获取特定具体值时执行操作时,此语法非常有用。
18.3.2. 匹配命名变量
命名的变量是可匹配任何值的无可辩驳模式。看个例子:
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 = {x:?}, y = {y}");
这个例子的逻辑很简单,主要是看这段代码中出现两个y
。这两个y
没有任何关系,处于不同的作用域,let y = 10
的y
就是为了存储10,而Some(y)
的y
主要是提取Option
类型下的Some
变体里附带的数据。
match
里的执行逻辑:
-
第一个分支中的模式与
x
的定义值不匹配,因此代码继续。 -
第二个匹配臂中的模式引入了一个名为
y
的新变量,它将匹配Some
值内的任何值。因为我们处于match
表达式内的新作用域,所以这是一个新的y
变量,而不是我们在开头声明的值为10的y
。这个新y
绑定将匹配Some
内的任何值,这就是我们所拥有的在x
中。因此,这个新的y
绑定到x
中Some
的内部值。该值为5
,因此该臂的表达式将执行并打印Matched, y = 5
。 -
如果
x
是None
值而不是Some(5)
(当然这个例子里不可能),则前两个臂中的模式将不匹配,因此该值将与下划线匹配。我们没有在下划线臂的模式中引入x
变量,因此表达式中的x
仍然是没有被遮蔽的外部x
。在这个假设的情况下,match
将打印Default case, x = None
。
输出:
Matched, y = 5
at the end: x = Some(5), y = 10
18.3.3. 多重模式
在match
表达式里,使用管道符|
语法(就是或的意思),可以匹配多种模式。看个例子:
let x = 1;
match x {
1 | 2 => println!("one or two"),
3 => println!("three"),
_ => println!("anything"),
}
例子中的第一个分支就是x
为1或2都能匹配。
18.3.4. 使用..=
来匹配某个范围的值
看例子:
let x = 5;
match x {
1..=5 => println!("one through five"),
_ => println!("something else"),
}
这个例子的第一个分支表示当x
值为1到5(闭区间),也就是1、2、3、4、5的任意一个时都会匹配。
由于 Rust 可以判断范围是否为空的唯一类型是char
和数值,因此范围仅允许包含数字或char
值。看个例子:
let x = 'c';
match x {
'a'..='j' => println!("early ASCII letter"),
'k'..='z' => println!("late ASCII letter"),
_ => println!("something else"),
}
这个例子的第一个分支代表匹配从a
到j
的字符,第二个分支代表匹配从k
到z
到字符。
18.3.5. 解构以分解值
我们可以使用模式来解构struct
、enum
和tuple
,从而引用这些类型值的不同部分。
解构struct
看个例子:
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);
}
Point
结构体下有两个字段x
和y
,都是i32
类型- 有一个
Point
的实例叫p
,其x
字段值为0,y
字段值为7 - 然后使用模式对
p
进行解构,x
的值被赋给a
,y
的值被赋给了b
这么写还是有些冗杂,如果把a
的变量名变为x
,b
的变量名变为y
,就可以简写为:
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);
}
解构还可以灵活地使用。看个例子:
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})");
}
}
}
- 第一个分支要求
x
字段值随意,y
字段值为0 - 第二个分支要求
x
字段值为0,y
字段值随意 - 第三个分支对
x
和y
的值无要求
解构enum
看例子:
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 {x} and in the y direction {y}");
}
Message::Write(text) => {
println!("Text message: {text}");
}
Message::ChangeColor(r, g, b) => {
println!("Change the color to red {r}, green {g}, and blue {b}")
}
}
}
该代码将打印 Change the color to red 0, green 160, and blue 255
。
解构嵌套的struct
和enum
看例子:
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 color to red {r}, green {g}, and blue {b}");
}
Message::ChangeColor(Color::Hsv(h, s, v)) => {
println!("Change color to hue {h}, saturation {s}, value {v}")
}
_ => (),
}
}
Message
下的ChangeColor
变体附带的数据就是Color
枚举类型。使用match
表达式匹配时一层一层匹配好即可。match
的前两个分支外面都是ChangeColor
变体,里面分别对应Color
的两个变体,里面存的值都可以通过变量取出来。
解构struct
和tuple
看例子:
struct Point {
x: i32,
y: i32,
}
fn main(){
let ((feet, inches), Point { x, y }) = ((3, 10), Point { x: 3, y: -10 });
}
main
函数里的模式匹配外层是一个元组,这个元组有两个元素:
- 第一个元素是个元组,里面有两个元素
- 第二个是
Point
结构体
在模式中忽略值
有几种方式可以在模式中忽略整个值或部分值:
_
:忽略整个值_
配合其他模式:忽略部分值- 使用以
_
开头的名称 ..
:忽略值的剩余部分
使用_
来忽略整个值
看例子:
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
。
使用嵌套的_
来忽略值的一部分
看例子:
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
或new_setting_value
是 None
),把new_setting_value
变为setting_value
。这就是_
配合其他模式来忽略某个值。
我们还可以在一种模式中的多个位置使用下划线来忽略特定值。看例子:
let numbers = (2, 4, 8, 16, 32);
match numbers {
(first, _, third, _, fifth) => {
println!("Some numbers: {first}, {third}, {fifth}")
}
}
这里就忽略了元组的第2个和第4个元素。此代码将打印Some numbers: 2, 8, 32
,并且值4和16将被忽略。
使用_
开头命名来忽略未使用的变量
看例子:
fn main() {
let _x = 5;
let y = 10;
}
正常情况下如果你创建了变量但没有使用它Rust编译器会发出警告,_x
和y
就没被使用,但使用y
的警告。因为_x
使用_
开头告诉编译器这是个临时的变量。
请注意,仅使用_
和使用以下划线开头的名称之间存在细微差别。语法_x
仍然将值绑定到变量,而_
根本不绑定。看例子:
let s = Some(String::from("Hello!"));
if let Some(_s) = s {
println!("found a string");
}
println!("{s:?}");
我们会收到一个错误,因为s
值仍会被移动到_s
中,这会阻止我们打印s
。
对于这种情况,就应该使用_
来避免绑定值的操作:
let s = Some(String::from("Hello!"));
if let Some(_) = s {
println!("found a string");
}
println!("{s:?}");
使用..
来忽略值的剩余部分
看例子:
struct Point {
x: i32,
y: i32,
z: i32,
}
fn main(){
let origin = Point { x: 0, y: 0, z: 0 };
match origin {
Point { x, .. } => println!("x is {x}"),
}
}
使用match
匹配时之需要x
字段就可以了,所以模式匹配只写x
,其余用..
这么使用..
也是可以的:
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(first, .., last) => {
println!("Some numbers: {first}, {last}");
}
}
}
只取开头和结尾的两个值,其余忽略。
这么写..
是不行的:
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(.., second, ..) => {
println!("Some numbers: {second}")
},
}
}
前面是..
,后面是..
,我要中间的元素。但具体是哪个元素呢?这么写编译器不知道..
具体要省略多少个元素,也就不明白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` (bin "patterns") due to 1 previous error
18.3.6. 使用match guards
(match
守卫)来提供额外的条件
match guards
是match
分支模式后一个附加的if
条件,想要匹配该分支该条件也必须能满足。match guards
适用于比单独的模式更复杂的场景。
看例子:
fn main(){
let num = Some(4);
match num {
Some(x) if x % 2 == 0 => println!("The number {x} is even"),
Some(x) => println!("The number {x} is odd"),
None => (),
}
}
在match
的第一个分支中,Some(x)
是模式,而if x % 2 == 0
就是match guards
,要求Some
附带的数据要能被2整除。
无法在模式中表达if x % 2 == 0
条件,因此match guards
使我们能够表达此逻辑。这种额外表达能力的缺点是,当涉及匹配保护表达式时,编译器不会尝试检查是否详尽。
看第二个例子:
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 = {x:?}, y = {y}");
}
此代码现在将打印Default case, x = Some(5)
。
match
守卫if n == y
不是模式,因此不会引入新变量。这个y
是外部y
(值为10)而不是新的阴影y
,我们可以通过比较来查找与外部y
具有相同值的值 n
到y
。
看第三个例子:
let x = 4;
let y = false;
match x {
4 | 5 | 6 if y => println!("yes"),
_ => println!("no"),
}
这个例子配合多重模式来使用match
守卫。
匹配条件规定,仅当x
的值等于4
、 5
或6
且y
为true
时,该分支才匹配。当此代码运行时,因为x
是4
,但匹配守卫y
为false
,所以不会执行第一个分支而会执行第二个分支输出no
。
这个例子里需要注意的是匹配模式相对于match
守卫的优先级,其优先级应该是:
(4 | 5 | 6) if y => ...
而不是:
4 | 5 | (6 if y) => ...
18.3.7. @
绑定
@
符号让我们可以创建一个变量,该变量可以在测试某个值是否与模式匹配的同时保存该值。
看例子:
enum Message {
Hello { id: i32 },
}
fn main(){
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}"),
}
}
这个例子match
的第一个分支在模式匹配时既把id
字段的值绑定在id_varible
上又判断了其值应该在3到7的闭区间内。