【Rust】——所有的模式语法
💻博主现有专栏:
C51单片机(STC89C516),c语言,c++,离散数学,算法设计与分析,数据结构,Python,Java基础,MySQL,linux,基于HTML5的网页设计及应用,Rust(官方文档重点总结),jQuery,前端vue.js,Javaweb开发,Python机器学习等
🥏主页链接:
目录
🎯匹配字面值
可以直接匹配字面值模式。
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 = {y}", x);
让我们看看当
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
。
🎯多个模式
在
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 | 2 | 3 | 4 | 5
。相反指定范围就简短的多,特别是在希望匹配比如从 1 到 1000 的数字的时候!编译器会在编译时检查范围不为空,而
char
和数字值是 Rust 仅有的可以判断范围是否为空的类型,所以范围只允许用于数字或char
值。let x = 'c'; match x { 'a'..='j' => println!("early ASCII letter"), 'k'..='z' => println!("late ASCII letter"), _ => println!("something else"), }
🎯解构并分解值
🎃解构结构体
有两个字段
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;
包含了很多重复,所以对于匹配结构体字段的模式存在简写:只需列出结构体字段的名称,则模式创建的变量会有相同的名称。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
)或不在任何轴上的点。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
表达式一旦找到一个匹配的模式就会停止检查其它分支,所以即使Point { x: 0, y: 0}
在x
轴上也在y
轴上,这些代码也只会打印On the x axis at 0
。
🎃解构枚举
不过当时没有明确提到解构枚举的模式需要对应枚举所定义的储存数据的方式。
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
。尝试改变msg
的值来观察其他分支代码的运行。对于像
Message::Quit
这样没有任何数据的枚举成员,不能进一步解构其值。只能匹配其字面值Message::Quit
,因此模式中没有任何变量。对于像
Message::Move
这样的类结构体枚举成员,可以采用类似于匹配结构体的模式。在成员名称后,使用大括号并列出字段变量以便将其分解以供此分支的代码使用。
🎃解构嵌套的结构体和枚举
目前为止,所有的例子都只匹配了深度为一级的结构体或枚举,不过当然也可以匹配嵌套的项!
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}") } _ => (), } }
match
表达式第一个分支的模式匹配一个包含Color::Rgb
枚举成员的Message::ChangeColor
枚举成员,然后模式绑定了 3 个内部的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 时,当你需要特定类型签名但是函数实现并不需要某个参数时。这样可以避免一个存在未使用的函数参数的编译警告,就跟使用命名参数一样。
🎃使用嵌套的_忽略部分值
也可以在一个模式内部使用
_
忽略部分值,例如,当只需要测试部分值但在期望运行的代码中没有用到其他部分时。示例 18-18 展示了负责管理设置值的代码。业务需求是用户不允许覆盖现有的自定义设置,但是可以取消设置,也可以在当前未设置时为其提供设置。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
。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 不要警告未使用的变量,为此可以用下划线作为变量名的开头。示例 18-20 中创建了两个未使用变量,不过当编译代码时只会得到其中一个的警告:
fn main() { let _x = 5; let y = 10; }
这里得到了警告说未使用变量
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);
上面的代码能很好的运行;因为没有把
s
绑定到任何变量;它没有被移动。
🎃用..忽略剩余值
对于有多个部分的值,可以使用
..
语法来只使用特定部分并忽略其它值,同时避免不得不每一个忽略值列出下划线。..
模式会忽略模式中剩余的任何没有显式匹配的值部分。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
条件,它也必须被满足才能选择此分支。匹配守卫用于表达比单独的模式所能允许的更为复杂的情况。这个条件可以使用模式中创建的变量。示例 18-26 展示了一个
match
,其中第一个分支有模式Some(x)
还有匹配守卫if x % 2 == 0
(当x
是偶数的时候为真):let num = Some(4); match num { Some(x) if x % 2 == 0 => println!("The number {} is even", x), Some(x) => println!("The number {} is odd", x), None => (), }
上例会打印出
The number 4 is even
。当num
与模式中第一个分支比较时,因为Some(4)
匹配Some(x)
所以可以匹配。接着匹配守卫检查x
除以2
的余数是否等于0
,因为它等于0
,所以第一个分支被选择。相反如果
num
为Some(5)
,因为5
除以2
的余数是1
不等于0
所以第一个分支的匹配守卫为假。接着 Rust 会前往第二个分支,这次匹配因为它没有匹配守卫所以会匹配任何Some
成员。无法在模式中表达类似
if x % 2 == 0
的条件,所以通过匹配守卫提供了表达类似逻辑的能力。这种替代表达方式的缺点是,编译器不会尝试为包含匹配守卫的模式检查穷尽性。
🎯@绑定
at 运算符(
@
)允许我们在创建一个存放值的变量的同时测试其值是否匹配模式。示例 18-29 展示了一个例子,这里我们希望测试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
字段的值进行测试:任何值都会匹配此分支。