前言#
昨天我们接触到了rust里的面向对象编程知识
今天继续往下学
模式(Patterns)和匹配(Matching)#
模式是rust中的一种特殊语法,它被用来匹配类型的结构,既复杂又简单(呃....)。
使用模式搭配match表达式或者其它构造类型能改善我们对程序的控制流(control flow)的控制。
一个模式一般由以下某几项组合而成:
Literals:字面量,比如123这样的数字等,会直接硬编码进binary里面 ;Destructured arrays, enums, structs, or tuples: 解构数组、枚举、结构体或者元组;Variables:变量;Wildcards:通配符;Placeholders:占位符;
它们都可以用来表示某个数据的样子/状态,比如Some(Color::Red)、x或者123。
然后我们可以匹配它们,判断这些个数据在匹配的上下文是否是正常的。
其实我们之前就学过了匹配最常用的match表达式,或者if let等。
接下来将会深入的了解模式和匹配。
所有的地方都可以使用模式(Patterns)#
其实看到上面模式的组成部分,你就应该已经知道了我们之前就一直在用模式了,比如声明一个变量let x = 123;这个变量就是一个模式。
match arms#
这个不知道咋翻译,干脆不翻译了。
我们来回顾下match的用法
match VALUE {
PATTERN => EXPRESSION,
PATTERN => EXPRESSION,
PATTERN => EXPRESSION,
}PATTERN => EXPRESSION就是一个arm,而箭头左边就是一个Pattern,表示被匹配的对象,既模式。
稍微有那么一丁点抽象,我们直接来看个例子
match x {
None => None,
Some(i) => Some(i + 1),
}我们match了x这个Option类型的枚举。
根据前面模式的组成部分,枚举也是一个pattern。
另外回顾下之前我们学的match,如果一个枚举类型存在多个字段,那么我们在match匹配的时候就需要把这些字段都匹配了才行。
当然,也不全是这样,我们还学了_占位符。_占位符仅仅表示一个占位符而已,表示其它场景我们不在乎,它不能作为变量,如果你尝试去使用这个通配符就会报错。
比如:
match x {
Some(i) => Some(i + 1),
_ => None,
}我们等会将接触更多的和_占位符相关的知识。
除了下划线通配符,我们之前还学了if let的用法。
if let条件表达式#
我们之前学过它是用来简写,表示我们只在乎某个场景,其它场景都不处理。
比如:
if let x = Some(2) {
dbg!(x);
}其实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, {color}, as the background");
} 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");
}
}
我们模拟了下根据最喜欢的颜色、今天是否是星期二(今天几号?16号咯~)以及用户的年龄来返回用户应该会使用什么背景颜色。
优先判断用户最喜欢的颜色,然后是日期,最后才是判断年龄。
目前模拟的结果应该是purple,打印看一下。

正常。
可以看到不仅有if let还有else if let和else if,它们可以混用的。
混用这个词不是很贴切,其实在rust中else 后面可以直接接一个表达式,所以看起来就像是在混用。
这里还有一个点需要说下,let Ok(age) = age,前面我们声明过了age这个变量,现在我们又重新声明了一个age,值是Ok(age)里的age。 所以后面age > 30这个并不会报错。
使用if let还挺方便的,不过有一个点需要注意,那就是我们没有把匹配的所有情况都列举出来,并且编译器不会报错,这样就有可能存在业务相关的bug。
while let条件循环#
类似if let,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源码

可以看到返回的是Option的类型,并且每次的len都自减1,当为0的时候返回None。
那么while就会执行len次。

表现符合预期。
for循环#
for循环中遍历的元素就是一个模式,比如
let v = vec!['a', 'b', 'c'];
for (index, value) in v.iter().enumerate() {
println!("{} is at index {}", value, index);
} 这里的index和value就是模式,它们其实算是变量,匹配v里面的元素。
而这个例子中我们还使用了解构,把enumerate返回的结构体Enumerate给解构了

至于这里为啥是个元组

我想应该是因为这里。
扯远了,回到我们的代码
let语句#
let语句因为声明了变量,自然也是一个模式匹配。
比如:
#![allow(unused)]
fn main() {
let x = 5;
}这个x就是一个模式。
它可以抽象成这样
let PATTERN = EXPRESSION;我们可以来看个更能说明这是一个模式匹配的例子
fn main() {
let (x, y, z) = (1, 2, 3);
}元组的解构赋值,x匹配到1,y匹配到2,z匹配到3。
元组的解构是一视同仁的,你必须把所有的元素都解构出来才行,不然编译器会直接报错。
不过我们后面会了解到一种方式可以忽略某些元素,只关注我们想要的元素,毕竟这太不人性化了。
函数参数#
基本上有变量的地方就有模式。函数的参数也算是一种变量,它匹配了传入的数据并bind这个数据。
我们来看个例子
fn print_coordinates(&(x, y): &(i32, i32)) {
println!("Current location: ({}, {})", x, y);
}
fn main() {
let point = (3, 5);
print_coordinates(&point);
}print_coordinates这个方法接受一个元组的引用,那自然就可以解构了。x匹配到了元组的第一个元素,y匹配到了元组的第二个元素。
闭包同理。
可辩驳性(Refutability): 一个模式可能匹配失败#
既然有可辨驳性(refutability),那自然也有不可辩驳性(irrefutability)。
如果一个模式可以匹配任何的值,那这个模式就是不可辩驳的,这个词通俗的说就是:不可能失败的。
比如:
let x = 5;变量声明是不可辩驳的,它能匹配任何的值。
当然,编译器报错提示类型不对这个属于其它情况,这个需要我们手动去填写类型就可以解决,并不属于模式匹配失败的场景。
前面提到的函数参数、let语句以及for循环都是不可辩驳的。 为啥呢?
因为它们的模式理论上都是可以接收任何的值的。
那什么情况是可辨驳的呢?
直接来看下例子
let x = Some(1);
if let Some(x) = Some(1) {
println!("{x}");
}如果x变成了None,那它就匹配失败了。
所以if let和while let都是都可能可辨驳的,当然,它们也有可能不可辩驳,取决于写代码的你。
毕竟它们的模式上下文中有条件表达式,自然就可能匹配失败。
通常情况下,我们并不需要担心模式的可辨驳以及不可辩驳。
不过最好还是熟悉下,到时除了错误方便排查。
来看两个例子加强训练
fn main() {
let some_option_value: Option<i32> = None;
let Some(x) = some_option_value;
}我们使用了let语句,它是不可辩驳的,但是我们居然还加上了Some,太牛角尖了。
不可辩驳搭配可辨驳,直接量子力学。
编译器可不会让我们这么量子力学,直接乱棍打死。

这样你就不能写这种前后矛盾的语句了。
上面的代码可以改成if let来通过编译
fn main() {
let some_option_value: Option<i32> = None;
if let Some(x) = some_option_value {
}
}然后我们再来看个反过来的例子:可辨驳的搭配不可辩驳的。
fn main() {
if let x = 5 {
println!("{}", x);
};
}这样编译器不会报错,毕竟不可辩驳也可以算可辨驳的一个子集。
但是会给一个警告

模式(Pattern)语法(Syntax)#
前面我们说了好几种模式匹配的写法,我们将在这一章节来学习什么场景选择哪种更合适。
匹配字面量#
匹配字面量我们可以直接用match,比如
fn main() {
let x = 1;
match x {
1 => println!("one"),
2 => println!("two"),
3 => println!("three"),
_ => println!("anything"),
}
}
当箭头左边的模式是一个字面量的时候,并且是一连串的字面的的时候,用match就很合适了,比如字面量\枚举这些确定类型的。
在其他语言中大概率会是switch的写法(以js为例),用match优雅很多,相当于把switch改成了对象匹配{ ... }[xxx]这样。
匹配命名变量#
这个老熟人了,它是不可辩驳的,因为理论上它能接收任何的值。
如果还使用上面的match来写的话,就不太好了。
match表达式的{}是一个新的作用域。 如果你在这个作用域中声明一个同名变量,那在当前作用域中它就会覆盖外层作用域的同名变量。
比如:
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的值和外面作用域x的值是不一样的。而y则是外层作用域的x的Some保存的值。
这样的可阅读性就差很多了。不过一般情况下我们都不会这样写,虽然有作用域保护,但是会产生歧义。
那么要怎么改呢?我们之后会学到一个match guard conditional,匹配条件守卫。
现在先绕过。
多重模式(Multiple Pattern)#
当我们匹配字面量的时候,我们用match,然后每个arms都需要写出来。这时就存在一个问题,当某两个arms需要做的事情是一样的时候就会存在重复代码,虽然我们可以抽函数出来,但是这样就会把问题搞得复杂了,我们需要有类似switch允许多个条件之间不写break来复用同一块逻辑的功能。
这个时候多重模式就可以登场了
我们直接来看下例子
fn main() {
let x = 1;
match x {
1 | 2 => println!("one or two"),
3 => println!("three"),
_ => println!("anything"),
}
}使用|把多个需要执行相同逻辑的模式分隔开。
使用..=匹配一个范围#
之前我们也用过..=这个符号,在我们创建一个range的时候。
let a = 1..=4;
let b = 1..4;这也可以用在模式匹配中
fn main() {
let x = 5;
match x {
1..=5 => println!("one through five"),
_ => println!("something else"),
}
}如果这个值是在1~5之间,那它就会被匹配到。
另外字符的范围也是可以匹配到的
fn main() {
let x = 'c';
match x {
'a'..='j' => println!("early ASCII letter"),
'k'..='z' => println!("late ASCII letter"),
_ => println!("something else"),
}
}没想到吧~
解构#
对于前端开发来说,解构这个词大家应该都很熟悉了,这里就不多说了。
而在rust中我们已经接触过元组的解构了,实际上struct和enum也都是可以解构的。
先来看下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);
}有点类似前端对象解构时允许的命名空间,这里用a, b匹配x, 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);
}
直接使用原来的字段。
这也是一种匹配,模式是x, y。
另外解构也能被拿来match匹配,不过仅限于字面量的解构,为什么呢?因为解构的过程产生两个新变量,这个问题就回到匹配命名变量使用match了。
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})");
}
}
}
我们解构了x, y,当y为0的时候,匹配打印输出在x轴,同理当x为0的时候打印输出在y轴。
这里需要注意的是这俩arm各自只产生了一个变量,另一个则是字面量。
然后我们来看下解构枚举,直接来看下例子
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}",
),
}
}这个直接一目了然了,就不多说了。
然后我们再来看个套娃的枚举
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}"
),
_ => (),
}
}我们的ChangeColor变体的值是一个Color枚举,然后再match,我们解构了ChangeColor的值的变体的值。
可以看到一旦套娃过多,这里match的arm就会变得非常多。
我们还可以混搭它们的解构
解构structs和tuples#
前面套娃枚举能够解构,那么其它套娃场景也是可以解构的。
比如:、
fn main() {
struct Point {
x: i32,
y: i32,
}
let ((feet, inches), Point { x, y }) = ((3, 10), Point { x: 3, y: -10 });
}我们解构了一个元组,这个元组第一个元素也是元组,第二个则是结构体。然后我们都给它们解构了。
忽略模式中的某个值#
前面说过解构一个元组需要把元组里的所有元素都解构出来才行,但有时候这个元组巨长,我们并不想写这么多东西,这就很不人性。
我们在match的时候可以通过_占位符省略部分arm,而这个占位符其实也能用在其他地方,效果也是省略。
我们先来看个例子
fn foo(_: i32, y: i32) {
println!("This code only uses the y parameter: {}", y);
}
fn main() {
foo(3, 4);
}
我们省略了第一个参数。
和match中一样,这里的_不能被当做变量,只是占位而已。
我们省略了第一个参数,看起来好像有些大病。
实际上还有有用的,因为当我们在实现一个trait的时候,重写实现这个trait里的方法时,我们并不需要某个参数,这个时候就可以用占位符了。
我们还可以忽略多个参数,比如
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(first, _, third, _, fifth) => {
println!("Some numbers: {first}, {third}, {fifth}")
}
}
}解构的时候我们忽略了第二、第四个参数。
我们还可以忽略值,比如:
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);
}我们匹配这个元组,只需要这俩元素都是有效的就行了,其它我不关注。
另外我们还可以用下划线开头来存储某个未使用变量,让编译器不会提示。
fn main() {
let _x = 5;
let y = 10;
}y会被编译器警告,而_x不会。
不过需要注意一点,_和_xxx是有区别的,_只是一个占位符,而_xxx还是一个变量,来看个例子
fn main() {
let s = Some(String::from("Hello!"));
if let Some(_s) = s {
println!("found a string");
}
println!("{:?}", s);
}
这样是会报错的。

s里的值的所有权被转移到_s里了,这里再打印s自然就会报错。
而当我们把_s改为_之后就不会报错了。
_占位符挺方便的,但是有些场景还不够方便,比如如果一个元组有20个元素,我们只需要前几个,而这时解构就不可能了,而直接用点下标获取也需要写一堆重复代码,这个时候我们就需要另一种省略方法。
使用..忽略剩余部分。#
直接来看下例子
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,按照之前的写法,我们得这样写{ x, y: _, z: _ },现在我们直接..就好了。
然后我们再来看下解构元组里的用法
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我们压根不知道是哪个位置的元素,它可能是第二个第三个第四个。
Match Guards#
match guards是一个特殊的if条件,跟在match的arm后面,我们直接来看下例子
fn main() {
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 => (),
}
}和之前的例子不同的地方在于我们Some(x)之后还加了一条条件表达式if x % 2 == 0,并且在下一个arm我们还是在匹配Some(x)。
这个条件表达式就是一个match guard,在匹配到arm之后又加了一层保险,还得是2的倍数才行。
这就很符合“守卫”这个词了。
这么做就使得match表达式变得灵活了,不过随之而来的缺点就是编译器不会再穷举所有的可能性,毕竟如果存在俩arm长得一样的按原来的规则早就乱棍打死了。
还记得我们在匹配命名变量这一小章节留的小坑吗?
我们来用match guard改下
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 = {y}", x);
}
我们用n代替原来的y,然后加上守卫if n == y。这样就能重命名变量了而不会shadow外层作用域的y,
现在的可阅读性高了很多。
只要在match表达式里的,就可以用守卫,我们之前学的比如多重模式就可以使用这个守卫再二次筛选
fn main() {
let x = 4;
let y = false;
match x {
4 | 5 | 6 if x == 4 => println!("yes"),
_ => println!("no"),
}
}虽然这么写有些大病。
这个arm看起来像是4 | 5 | (6 if x == 4),实际上并不是,更接近于(4 | 5 | 6) if x == 4。
@绑定#
我们直接来看下例子
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),
}
}然后打印下结果

我们先来解释下代码,Message这个枚举的Hello变体存储的值类型是一个struct。
我们用match匹配这个struct,第一个arm中我们解构了id,命名为id_variable,但是我们后面还加上了@以及一个范围3..=7。
而根据我们的输出结果,这个id_variable的值是5也就是我们匹配的实例的id的值。
把这个arm和下面第二第三个arm对比,相信大家都已经猜到这个@操作符是用来干嘛的了,就是用来把匹配的值赋值给变量的。
之前我们匹配解构的字面量或者范围的时候我们没有办法使用这个匹配的值,因为它并不是一个变量。
而现在,基于@操作符,我们可以即解构了字面量又可以用这个数据。
总结#
这一章节我们主要是学匹配/解构的方式,以及模式和匹配的概念。
相对最近学的几章来说比较简单,也算是一个过渡吧,下一章的知识点是所有章节最多的。
参考#
- ^rust-Patterns-and-Matching https://doc.rust-lang.org/book/ch18-00-patterns.html
- ^rust-all-the-places-Patterns-can-be-used https://doc.rust-lang.org/book/ch18-01-all-the-places-for-patterns.html#all-the-places-patterns-can-be-used
- ^rust-match-arms https://doc.rust-lang.org/book/ch18-01-all-the-places-for-patterns.html#match-arms
- ^rust-conditional-if-let-expressions https://doc.rust-lang.org/book/ch18-01-all-the-places-for-patterns.html#conditional-if-let-expressions
- ^rust-while-let-conditional-loop https://doc.rust-lang.org/book/ch18-01-all-the-places-for-patterns.html#while-let-conditional-loops
- ^rust-18.1-for-loops https://doc.rust-lang.org/book/ch18-01-all-the-places-for-patterns.html#for-loops
- ^rust-let-Statements https://doc.rust-lang.org/book/ch18-01-all-the-places-for-patterns.html#let-statements
- ^rust-function-parameters https://doc.rust-lang.org/book/ch18-01-all-the-places-for-patterns.html#function-parameters
- ^rust-Refutability https://doc.rust-lang.org/book/ch18-02-refutability.html#refutability-whether-a-pattern-might-fail-to-match
- ^rust-Pattern-Syntax https://doc.rust-lang.org/book/ch18-03-pattern-syntax.html#pattern-syntax
- ^rust-matching-literals https://doc.rust-lang.org/book/ch18-03-pattern-syntax.html#matching-literals
- ^rust-matching-named-variable https://doc.rust-lang.org/book/ch18-03-pattern-syntax.html#matching-named-variables
- ^rust-multiple-pattern https://doc.rust-lang.org/book/ch18-03-pattern-syntax.html#multiple-patterns
- ^rust-match-ranges-values-with-..= https://doc.rust-lang.org/book/ch18-03-pattern-syntax.html#matching-ranges-of-values-with-
- ^rust-destructuring-break-apart-values https://doc.rust-lang.org/book/ch18-03-pattern-syntax.html#destructuring-to-break-apart-values
- ^rust-destructuring-structs-and-tuples https://doc.rust-lang.org/book/ch18-03-pattern-syntax.html#destructuring-structs-and-tuples
- ^rust-ignore-values-in-patterns https://doc.rust-lang.org/book/ch18-03-pattern-syntax.html#ignoring-values-in-a-pattern
- ^rust-Match-Guards https://doc.rust-lang.org/book/ch18-03-pattern-syntax.html#extra-conditionals-with-match-guards
- ^rust-@ Bindings https://doc.rust-lang.org/book/ch18-03-pattern-syntax.html#-bindings
发布于 2023-01-08 17:21・IP 属地广东
