Rust 语言从入门到实战 唐刚--读书笔记06
基础篇 (11讲)
06|复合类型(下):枚举与模式匹配
学习Rust 中的枚举(enum)和模式匹配(pattern matching)。
枚举是 Rust 中非常重要的复合类型,最强大的复合类型之一,用于属性配置、错误处理、分支流程、类型聚合等场景中。
枚举:强大的复合类型
枚举:容纳选项的可能性,每一种可能的选项都是一个变体(variant)。
用关键字 enum 定义,与 Java、C++ 一样。
Rust 中的枚举具有更强大的表达能力。
Rust 中,枚举中的所有条目被叫做这个枚举的变体。
enum Shape {
Rectangle,
Triangle,
Circle,
}
定义形状(Shape)枚举,三个变体:长方形 Rectangle、三角形 Triangle 和圆形 Circle。
枚举与结构体不同:
- 结构体的实例化:需要所有字段一起起作用,
- 枚举的实例化:只需要且只能是其中一个变体起作用。
负载
enum 中的变体可以挂载各种形式的类型。
所有其他类型,如字符串、元组、结构体等,都可作为 enum 的负载(payload)被挂载到其中一个变体上。
扩展上面的代码示例。
enum Shape {
Rectangle { width: u32, height: u32},
Triangle((u32, u32), (u32, u32), (u32, u32)),
Circle { origin: (u32, u32), radius: u32 },
}
给 Shape 枚举的三个变体都挂载了不同的负载。
Rectangle 挂载了一个结构体负载,表示宽和高的属性。
{width: u32, height: u32}
也可以单独定义一个结构体,再挂载到 Rectangle 变体上。
struct Rectangle {
width: u32,
height: u32
}
enum Shape {
Rectangle(Rectangle),
// ...
}
Triangle 变体挂载了一个元组负载 ((u32, u32), (u32, u32), (u32, u32)),表示三个顶点。
Circle 变体挂载了一个结构体负载 { origin: (u32, u32), radius: u32 },表示原点加半径长度。
枚举的变体能够挂载各种类型的负载,是 Rust 中的枚举超强能力的来源。
enum 就像一个筐,什么都能往里面装。
示例: WebEvent 表示浏览器里面的 Web 事件。
enum WebEvent {
PageLoad,
PageUnload,
KeyPress(char),
Paste(String),
Click { x: i64, y: i64 },
}
可以表述出不同变体的意义,还有每个变体所挂载的负载类型吗?
枚举的实例化
枚举的实例化实际是枚举变体的实例化。
let a = WebEvent::PageLoad;
let b = WebEvent::PageUnload;
let c = WebEvent::KeyPress('c');
let d = WebEvent::Paste(String::from("batman"));
let e = WebEvent::Click { x: 320, y: 240 };
不带负载的变体实例化和带负载的变体实例化不一样。
带负载的变体实例化,要根据不同变体附带的类型做特定的实例化。
类 C 枚举
定义类似 C 语言中的枚举:
// 给枚举变体一个起始数字值
enum Number {
Zero = 0,
One,
Two,
}
// 给枚举每个变体赋予不同的值
enum Color {
Red = 0xff0000,
Green = 0x00ff00,
Blue = 0x0000ff,
}
fn main() {
// 使用 as 进行类型的转化
println!("zero is {}", Number::Zero as i32);
println!("one is {}", Number::One as i32);
println!("roses are #{:06x}", Color::Red as i32);
println!("violets are #{:06x}", Color::Blue as i32);
}
// 输出
zero is 0
one is 1
roses are #ff0000
violets are #0000ff
能像 C 语言那样,在定义枚举变体的时候,指定具体的值。
这在底层系统级开发、协议栈开发、嵌入式开发的场景会经常用到。
打印的时候,只需用 as 操作符将变体转换为具体的数值类型即可。
代码中的 println! 里的 {:06x} 是格式化参数,打印值的 16 进制形式,占 6 个宽度,不足的用 0 补齐。
println 打印语句中,格式化参数相当丰富。
空枚举
Rust 中可以定义空枚举。如 enum MyEnum {}; 。
它与单元结构体一样,表示一个类型。但它不能被实例化。
enum Foo {}
let a = Foo {}; // 错误的
// 提示
expected struct, variant or union type, found enum `Foo`
not a struct, variant or union type
impl 枚举
Rust 中关键字 impl 可用来给结构体或其他类型实现方法,即关联在某个类型上的函数。第 5 讲
枚举同样能够被 impl。如:
enum MyEnum {
Add,
Subtract,
}
impl MyEnum {
fn run(&self, x: i32, y: i32) -> i32 {
match self { // match 语句
Self::Add => x + y,
Self::Subtract => x - y,
}
}
}
fn main() {
// 实例化枚举
let add = MyEnum::Add;
// 实例化后执行枚举的方法
add.run(100, 200);
}
但不能对枚举的变体直接 impl。
enum Foo {
AAA,
BBB,
CCC
}
impl Foo::AAA { // 错误的
}
一般,枚举用来做配置,结合 match 语句做分支管理。
如果要定义一个新类型,Rust 中主要使用结构体。
match
学习和枚举搭配使用的 match 语句。
match + 枚举
作用是判断或匹配值是哪一个枚举的变体。
#[derive(Debug)]
enum Shape {
Rectangle,
Triangle,
Circle,
}
fn main() {
let shape_a = Shape::Rectangle; // 创建实例
match shape_a { // 匹配实例
Shape::Rectangle => {
println!("{:?}", Shape::Rectangle); // 进了这个分支
}
Shape::Triangle => {
println!("{:?}", Shape::Triangle);
}
Shape::Circle => {
println!("{:?}", Shape::Circle);
}
}
}
// 输出
// Rectangle
可以改变实例为另外两种变体,看打印的信息有没有变化,判断走了哪个分支。
match 可返回值
match 可以有返回值, match 表达式。
#[derive(Debug)]
enum Shape {
Rectangle,
Triangle,
Circle,
}
fn main() {
let shape_a = Shape::Rectangle; // 创建实例
let ret = match shape_a { // 匹配实例,并返回结果给ret
Shape::Rectangle => {
1
}
Shape::Triangle => {
2
}
Shape::Circle => {
3
}
};
println!("{}", ret);
}
// 输出
// 1
shape_a 被赋值为 Shape::Rectangle,匹配到第一个分支并返回 1,ret 的值为 1。
let ret = match shape_a {
地道的 Rust 写法,更紧凑。
注意,match 表达式中各个分支返回的值的类型必须相同。
所有分支都必须处理
match 表达式里所有的分支都必须处理。
#[derive(Debug)]
enum Shape {
Rectangle,
Triangle,
Circle,
}
fn main() {
let shape_a = Shape::Rectangle; // 创建实例
let ret = match shape_a { // 匹配实例
Shape::Rectangle => {
1
}
Shape::Triangle => {
2
}
// Shape::Circle => {
// 3
// }
};
println!("{}", ret);
}
// 编译的时候会出错
error[E0004]: non-exhaustive patterns: `Shape::Circle` not covered
--> src/main.rs:10:19
|
10 | let ret = match shape_a { // 匹配实例
| ^^^^^^^ pattern `Shape::Circle` not covered
|
note: `Shape` defined here
--> src/main.rs:5:3
|
2 | enum Shape {
| -----
...
5 | Circle,
| ^^^^^^ not covered
= note: the matched value is of type `Shape`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
|
16 ~ },
17 + Shape::Circle => todo!()
|
提示:Shape::Circle 分支没有覆盖到
_ 占位符
不想处理一些分支,用 _ 偷懒:
#[derive(Debug)]
enum Shape {
Rectangle,
Triangle,
Circle,
}
fn main() {
let shape_a = Shape::Rectangle;
let ret = match shape_a {
Shape::Rectangle => {
1
}
_ => {
10
}
};
println!("{}", ret);
}
除 Shape::Rectangle 之外的分支,统一用 _ 占位符进行处理了。
更广泛的分支
match 除了配合枚举进行分支管理,还可与其他基础类型结合进行分支分派。
示例:
fn main() {
let number = 13;
// 你可以试着修改上面的数字值,看看下面走哪个分支
println!("Tell me about {}", number);
match number {
// 匹配单个数字
1 => println!("One!"),
// 匹配几个数字
2 | 3 | 5 | 7 | 11 => println!("This is a prime"),
// 匹配一个范围,左闭右闭区间
13..=19 => println!("A teen"),
// 处理剩下的情况
_ => println!("Ain't special"),
}
}
match 可用来匹配一个具体的数字、一个数字的列表,或一个数字的区间等等,非常灵活。
比 C、C++,或 Java 的 switch .. case 灵活多了。
模式匹配
模式匹配是按对象值的结构进行匹配,并且可以取出符合模式的值。
match 实际是模式匹配的入口。模式匹配不限于在 match 中使用。
除了 match ,Rust 还给模式匹配提供了其他一些语法层面的设施。
if let
当要匹配的分支只有两个或在这个位置只想先处理一个分支的时候,可直接用 if let。
let shape_a = Shape::Rectangle;
match shape_a {
Shape::Rectangle => {
println!("1");
}
_ => {
println!("10");
}
};
改写为:
let shape_a = Shape::Rectangle;
if let Shape::Rectangle = shape_a {
println!("1");
} else {
println!("10");
}
相比于 match,用 if let 的代码更简化
while let
while 后面跟 let,实现模式匹配:
#[derive(Debug)]
enum Shape {
Rectangle,
Triangle,
Circle,
}
fn main() {
let mut shape_a = Shape::Rectangle;
let mut i = 0;
while let Shape::Rectangle = shape_a { // 注意这一句
if i > 9 {
println!("Greater than 9, quit!");
shape_a = Shape::Circle;
} else {
println!("`i` is `{:?}`. Try again.", i);
i += 1;
}
}
}
// 输出
`i` is `0`. Try again.
`i` is `1`. Try again.
`i` is `2`. Try again.
`i` is `3`. Try again.
`i` is `4`. Try again.
`i` is `5`. Try again.
`i` is `6`. Try again.
`i` is `7`. Try again.
`i` is `8`. Try again.
`i` is `9`. Try again.
Greater than 9, quit!
示例构造了一个 while 循环,手动维护计数器 i,递增到 9 之后,退出循环。
看起来,在条件判断语句那里用 while Shape::Rectangle == shape_a 也行,好像用 while let 的意义不大。我们来试一下,编译之后,报错了。
error[E0369]: binary operation `==` cannot be applied to type `Shape`
说 == 号不能作用在类型 Shape 上,你可以思考一下为什么。
如果一个枚举变体带负载,用模式匹配可以把这个负载取出来,比较方便。
let
let 本身就支持模式匹配。if let、while let 使用的就是 let 模式匹配的能力。
#[derive(Debug)]
enum Shape {
Rectangle {width: u32, height: u32},
Triangle,
Circle,
}
fn main() {
// 创建实例
let shape_a = Shape::Rectangle {width: 10, height: 20};
// 模式匹配出负载内容
let Shape::Rectangle {width, height} = shape_a else {
panic!("Can't extract rectangle.");
};
println!("width: {}, height: {}", width, height);
}
// 输出
width: 10, height: 20
示例中,用模式匹配解开了 shape_a 中带的负载(结构体负载),同时定义了 width 和 height 两个局部变量,并初始化为枚举变体的实例负载的值。这两个局部变量在后续的代码块中可以使用。
注意第 12 行。
let Shape::Rectangle {width, height} = shape_a else {
是匹配结构体负载,获取字段值的方式。
匹配元组
元组也可以被匹配:
fn main() {
let a = (1,2,'a');
let (b,c,d) = a;
println!("{:?}", a);
println!("{}", b);
println!("{}", c);
println!("{}", d);
}
元组的析构,从函数的多个返回值里取出数据。
fn foo() -> (u32, u32, char) {
(1,2,'a')
}
fn main() {
let (b,c,d) = foo();
println!("{}", b);
println!("{}", c);
println!("{}", d);
}
匹配枚举
用 let 把枚举里变体的负载解出来:
struct Rectangle {
width: u32,
height: u32
}
enum Shape {
Rectangle(Rectangle),
Triangle((u32, u32), (u32, u32), (u32, u32)),
Circle { origin: (u32, u32), radius: u32 },
}
fn main() {
let a_rec = Rectangle {
width: 10,
height: 20,
};
// 请打开下面这一行进行实验
//let shape_a = Shape::Rectangle(a_rec);
// 请打开下面这一行进行实验
//let shape_a = Shape::Triangle((0, 1), (3,4), (3, 0));
let shape_a = Shape::Circle { origin: (0, 0), radius: 5 };
// 这里演示了在模式匹配中将枚举的负载解出来的各种形式
match shape_a {
Shape::Rectangle(a_rec) => { // 解出一个结构体
println!("Rectangle {}, {}", a_rec.width, a_rec.height);
}
Shape::Triangle(x, y, z) => { // 解出一个元组
println!("Triangle {:?}, {:?}, {:?}", x, y, z);
}
Shape::Circle {origin, radius} => { // 解出一个结构体的字段
println!("Circle {:?}, {:?}", origin, radius);
}
}
}
// 输出
Circle (0, 0), 5
将变体中的结构体整体、元组各部分、结构体各字段解析出来的方式。
可在做分支处理时,顺便处理携带的信息,让代码变得相当紧凑而有意义(高内聚)。要熟悉并掌握这些写法。
匹配结构体
了解结构体字段匹配过程中的一个细节。
#[derive(Debug)]
struct User {
name: String,
age: u32,
student: bool
}
fn main() {
let a = User {
name: String::from("mike"),
age: 20,
student: false,
};
let User {
name,
age,
student,
} = a;
println!("{}", name);
println!("{}", age);
println!("{}", student);
println!("{:?}", a);
}
编译输出:
error[E0382]: borrow of partially moved value: `a`
--> src/main.rs:24:22
|
16 | name,
| ---- value partially moved here
...
24 | println!("{:?}", a);
| ^ value borrowed here after partial move
|
= note: partial move occurs because `a.name` has type `String`, which does not implement the `Copy` trait
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: borrow this binding in the pattern to avoid moving the value
|
16 | ref name,
| +++
出错了,在模式匹配的过程中发生了 partially moved(部分移动)。模式匹配过程中新定义的三个变量 name、age、student 分别得到了对应 User 实例 a 的三个字段值的所有权。
age 和 student 采用了复制所有权的形式(参考第 2 讲移动还是复制部分),而 name 字符串值则是采用了移动所有权的形式。a.name 被部分移动到了新的变量 name ,所以接下来 a.name 就无法直接使用了。
示例说明 Rust 中的模式匹配是一种释放原对象的所有权的方式。
ref 关键字
建议添加一个关键字 ref 。
#[derive(Debug)]
struct User {
name: String,
age: u32,
student: bool
}
fn main() {
let a = User {
name: String::from("mike"),
age: 20,
student: false,
};
let User {
ref name, // 这里加了一个ref
age,
student,
} = a;
println!("{}", name);
println!("{}", age);
println!("{}", student);
println!("{:?}", a);
}
// 输出
mike
20
false
User { name: "mike", age: 20, student: false }
打印出了正确的值。
通过 ref 关键字修饰符告诉编译器,只需获得那个字段的引用,不要给我所有权。
ref 出现的原因,用来在模式匹配过程中提供一个额外的信息。
用了 ref ,新定义的 name 变量的值其实是 &a.name ,而不是 a.name,Rust 就不会再把所有权给 move 出来了,也不会发生 partially moved 这种事情,原来的 User 实例 a 还有效,能被打印出来了。你可以体会一下其中的区别。
相应的,还有 ref mut 的形式。用于在模式匹配中获得目标的可变引用。
let User {
ref mut name, // 这里加了一个ref mut
age,
student,
} = a;
可以做做实验体会一下。
Rust 中强大的模式匹配这个概念,来自于函数式语言。可以了解一下 Ocaml、Haskell 或 Scala 中模式匹配的相关概念。
函数参数中的模式匹配
函数参数其实就是定义局部变量,因此模式匹配的能力在这里也能得到体现。
示例 1:
fn foo((a, b, c): (u32, u32, char)) { // 注意这里的定义
println!("{}", a);
println!("{}", b);
println!("{}", c);
}
fn main() {
let a = (1,2, 'a');
foo(a);
}
把元组 a 传入了函数 foo(),foo() 的参数直接定义成模式匹配,解析出了 a、b、c 三个元组元素的内容,并在函数中使用。
示例 2:
#[derive(Debug)]
struct User {
name: String,
age: u32,
student: bool
}
fn foo(User { // 注意这里的定义
name,
age,
student
}: User) {
println!("{}", name);
println!("{}", age);
println!("{}", student);
}
fn main() {
let a = User {
name: String::from("mike"),
age: 20,
student: false,
};
foo(a);
}
把结构体 a 传入了函数 foo(),foo() 的参数直接定义成对结构体的模式匹配,解析出了 name、age、student 三个字段的内容,并在函数中使用。
小结
枚举是 Rust 中的重要概念,广泛用于属性配置、错误处理、分支流程、类型聚合等。
实际场景中,一般把结构体作为模型的主体承载,把枚举作为周边的辅助配置和逻辑分类。它们经常会搭配使用。
模式匹配是 Rust 里非常有特色的语言特性,在做分支逻辑处理的时候,可以通过模式匹配带上要处理的相关信息,还可以把这些信息解析出来,让代码的逻辑和数据内聚得更加紧密,让程序看起来更加赏心悦目。
思考题
match 表达式的各个分支中,如果有不同的返回类型的情况,应该如何处理?
## 参考资料
格式化参数:https://shimo.im/outlink/gray?url=https%3A%2F%2Fdoc.rust-lang.org%2Fstd%2Ffmt%2Findex.htmlmatch 的语法规则:https://doc.rust-lang.org/reference/expressions/match-expr.html