Rust模式探索:写出更优雅的Rust代码

模式定义

在Rust中,模式匹配是一种强大的编程工具,它允许你根据数据的结构来选择不同的执行路径。模式可以用在 match 表达式、if let 表达式、while let 表达式、函数参数、let 语句等地方

一个示例

来看个上一篇文章 Rust 枚举 简单入门 中例子

1
2
3
4
5
6
7
8
9
10
11
12
13
enum Color {
Red,
Orange,
Yellow
}

let c1 = Color::Red;

match c1 {
Color::Red => println!("Red"),
Color::Orange => println!("Orange"),
Color::Yellow => println!("Yellow")
}

match 会执行模式匹配,在此示例中,模式就是出现在 => 符号前面的部分,模式匹配可以和枚举协同工作,甚至可以测试它们包含的数据

模式类型

上面的例子是匹配枚举值的模式。模式的类型不止于此,Rust 模式还有它们自己的小型语言,如下表

模式类型 例子 注意事项
字面量 100 "name" 匹配一个确切的值;也允许匹配常量名称
范围 0 ..= 100 'a' ..= 'k' 256.. 匹配范围内的任何值,包括可能给定的结束值
通配符 _ 匹配任何值并忽略它
变量 name mut count 类似于 _,但会把值移动或复制到新的局部变量中
引用变量 ref field ref mut field 借用对匹配值的引用,而不是移动或复制它
与子模式绑定 val @ 0 ..= 99 ref circle @ Shape::Circle { .. } 使用 @ 左边的变量名,匹配其右边的模式
枚举型模式 Some(value) None Pet::Orca
元组型模式 (key, value) (r, g, b)
数组型模式 [a, b, c, d, e, f, g] [heading, carom, correction]
切片型模式 [first, second] [first, _, third] [first, .., nth] [ ]
结构体型模式 Color(r, g, b) Point { x, y } Card { suit: Clubs, rank: n } Account { id, name, .. }
引用 &value &(k, v) 仅匹配引用值
或多个模式 'a' 竖线 'A' Some("left" 竖线 "right")
守卫表达式 x if x * x <= r2 只用在 match 表达式中(不能用在 let 语句等处)

注意!

由于当前页面的 Markdown 格式转换问题,竖线 | 会导致排版异常,因此上面表格使用中文 竖线 代替 |

字面量、变量、通配符

字面量可以是整数、浮点数、字符、字符串、布尔值等。下面是整数字面量的一个例子

1
2
3
4
5
6
7
8
let x = 5;

match x {
1 => println!("one"),
2 => println!("two"),
3 => println!("three"),
_ => println!("something else"),
}

用单个下划线 _ 作为模式,这就是通配符模式,这里的通配符模式能匹配任意值,但不会将其存储到任何地方

即使你非常确定其他情况不会发生,也必须至少添加一个后备分支,也许是 panic 的分支

1
2
3
4
5
6
let s = "hello";

match s {
"hello" => println!("found hello"),
other => println!("no match"),
}

这里面的 other 是一个变量名,它可以匹配任何值,匹配的值会移动或复制到一个新的局部变量中,这些模式类似 switch 语句中的 default 分支,用于匹配与任何其他模式都无法匹配的值

元组型和结构体型

元组模式是一种模式,用于匹配元组的结构。元组模式由一对圆括号和一组模式组成,模式之间用逗号分隔

1
2
3
4
5
6
let x = (1, 2, 3);

match x {
(1, 2, 3) => println!("one, two, three"),
(a, b, c) => println!("({}, {}, {})", a, b, c),
}

结构体模式用于匹配结构体的结构。结构体模式由结构体的名称和一组模式组成,模式之间用逗号分隔

1
2
3
4
5
6
7
8
9
10
11
12
struct Point {
x: i32,
y: i32,
}

let p = Point { x: 0, y: 7 };

match p {
Point { x, y: 0 } => println!("x is {}, y is on the x axis", x),
Point { x: 0, y } => println!("x is on the y axis, y is {}", y),
Point { x, y } => println!("x is {}, y is {}", x, y),
}

当想要匹配一个大型结构体的一部分字段,而不是全部字段时,可以使用 .. 来表示剩余的字段。这被称为结构体模式的 .. 简写

1
2
3
4
5
6
7
8
9
10
11
12
struct Point {
x: i32,
y: i32,
z: i32,
// ... 其他字段
}

let p = Point { x: 0, y: 7, z: 0 };

match p {
Point { x, .. } => println!("x is {}", x),
}

match 表达式只关心 px 字段,而不关心 yz 字段。.. 表示剩余的字段,所以Point { x, ..  }匹配任何 Point 结构体,只要它的 x 字段匹配

数组型和切片型

数组型模式匹配数组。数组型模式通常用于过滤一些特殊情况的值,并且在处理那些不同位置的值具有不同含义的数组时也非常有用

1
2
3
4
5
6
let arr = [1, 2, 3];

match arr {
[1, 2, 3] => println!("found 1, 2, 3"),
_ => println!("no match"),
}

注意!

数组模式只能用于固定大小的数组,不能用于动态大小的数组(也就是切片)。如果你想要匹配一个切片的结构,你应该使用切片模式

切片型模式与数组型相似,但与数组不同,切片具有可变长度,因此切片型模式不仅匹配值,还匹配长度。.. 在切片型模式中能匹配任意数量的元素

1
2
3
4
5
6
7
8
let arr = ["a"];

match &arr[..] {
[] => println!("no body"),
[a] => println!("a"),
[a, b] => println!("a, b"),
_ => println!("no match"),
}

引用型模式

引用型模式(Reference Patterns)允许你通过引用来匹配和解构数据,而不是通过值。这种模式在处理借用的数据时特别有用,因为它允许你在不获取所有权的情况下访问数据的部分或全部内容

基本用法

引用型模式通常与&符号一起使用,表示你正在匹配一个引用。当你想要在模式匹配中解构一个引用指向的值时,这非常有用,下面是个简单的例子

1
2
3
4
5
let reference = &10;

match reference {
&val => println!("val: {:?}", val)
}

reference是一个指向10的引用。在match表达式中,模式&val用于解构reference,允许直接访问它指向的值10

解构数据

引用型模式在解构复杂数据结构时尤其有用,比如元组或结构体

1
2
3
4
5
let tuple = &(1, 2, 3);

match tuple {
&(x, y, z) => println!("Matched: {}, {}, {}", x, y, z),
}

使用ref关键字

ref关键字用于创建一个引用指向模式匹配中的值,而不是通过值绑定

1
2
3
4
5
6
let value = 5;
let ref reference = value;

match reference {
&val => println!("val: {:?}", val),
}

ref和mut结合使用

ref mut可以用来匹配可变引用,并允许修改通过引用访问的数据

1
2
3
4
5
6
7
8
9

let mut value = 5;

match value {
ref mut r => {
*r += 10;
println!("value: {}", r);
}
}
注意!
  • 使用ref mut时,必须确保被引用的数据本身是可变的
  • 修改通过ref mut创建的引用所指向的数据时,需要使用解引用操作符*
  • 在模式匹配中使用ref和ref mut可以让你更灵活地处理数据,特别是在需要引用而不是所有权的场景中

匹配守卫

匹配守卫(match guards)是一种与模式匹配结合使用的条件表达式,它提供了额外的条件来决定是否应该选择某个分支。这使得模式匹配更加灵活,允许在模式本身无法表达的复杂情况下进行精细的控制

匹配守卫紧跟在模式之后,使用if关键字引入,如下例子

1
2
3
4
5
6
7
let tuple = (5, 12);

match tuple {
(5, y) if y > 10 => println!("第一个元素是5,且第二个元素大于10,y = {}", y),
_ => println!("不匹配"),
}

在循环中使用匹配守卫

1
2
3
4
5
6
7
8
let numbers = vec![Some(10), Some(11), Some(20), Some(30)];

for option in numbers.iter() {
match option {
Some(x) if *x > 10 => println!("大于10的数字为:{}", x),
_ => (),
}
}

匹配多种可能性

模式匹配(Pattern Matching)是一种强大的控制流工具,它不仅可以匹配单一的值,还可以同时匹配多种可能性。这通过使用|运算符来实现,|在这里表示“或”(or),允许在同一个match分支中指定多个模式

1
2
3
4
5
6
let pair = (2, 3);
match pair {
(x, y) if x == y => println!("数字一样"),
(x, 0) | (0, x) => println!("x: {}", x),
_ => println!("没有匹配")
}

使用@模式绑定

@模式绑定的基本语法是在模式中使用@后跟一个变量名,这样可以在模式匹配成功时,将匹配到的值绑定到这个变量

1
2
3
4
5
6
7
8
9
10
11
12
13
enum Message {
Move { x: i32, y: i32 },
ChangeColor(i32, i32, i32)
}

let move_message = Message::Move { x: 3, y: 104 };

match move_message {
Message::Move { x: x_pos @ 0..=100, y: y_pos @ 101..=200 } => {
println!("移出({}, {})", x_pos, y_pos)
},
_ => println!("other")
}

模式能用在哪里

尽管模式在 match 表达式中作用最为突出,但它们也可以出现在其他一些地方,通常用于代替标识符。但无论出现在哪里,其含义都是一样的:Rust 不是要将值存储到单个变量中,而是使用模式匹配来拆分值

1
2
3
4
5
6
7
8
9
10
// 把结构体解包成3个局部变量……
let Track { album, track_number, title, .. } = song;

// ……解包某个作为函数参数传入的元组
fn distance_to((x, y): (f64, f64)) -> f64 { ... }

// ……迭代某个HashMap上的键和值
for (id, document) in &cache_map {
println!("Document #{}: {}", id, document.title);
}

上述示例中的每一个都节省了两三行样板代码。同样的概念也存在于其他一些语言中:JavaScript 中叫作解构,而 Python 中叫作解包

看到这里,有没有越看越顺的感觉? 😁

欢迎大家讨论交流,如果喜欢本文章或感觉文章有用,动动你那发财的小手点赞、收藏、关注再走呗 ^_^ 

微信公众号:草帽Lufei
掘金:草帽lufei