上节内容回顾
在我在外包学 rust——结构体 struct中主要介绍了 Rust 中的结构体相关知识,包括结构体的定义、实例化、形式(命名结构体、元组结构体、单元结构体)、所有权、添加标注,以及结构体的面向对象特性(方法、self 用法、关联函数、构造函数、Default)等内容。
这篇文章主要介绍 rust 的另一种复合类型——枚举。并由枚举引出 match 的相关内容,然后再引出模式匹配的内容。
枚举
枚举是 rust 中有别于结构体的另一种符合类型,广泛用于属性配置、错误处理、分支流程、类型聚合等。
枚举用于容纳选项的可能性,每种可能的选项都是一个变体(variant)。用 enum 关键字来定义。
结构体与枚举有什么区别呢? 结构体的实例化需要所有字段一起起作用。枚举的实例化只需要且只能是其中一个变体起作用。
enum Shape {
Rectangle,
Triangle,
Circle,
}
负载
变体可以挂载各种形式的类型。字符串、元祖、结构体都可以作为 enum 的负载(payload)被挂载到变体上。
enum Shape {
Rectangle { width: i32, height: i32 },
Triangle((u32, u32),(u32, u32),(u32, u32)),
circle { origin: (u32, u32), radius: u32 },
}
当然负载也可以是一个定义好的结构体:
struct Rectangle {
width: i32,
height: i32,
}
enum Shape {
Rectangle(Rectangle),
...
}
枚举的变体能够挂载各种类型的负载,赋予了 rust 的枚举超强能力。
枚举的实例化
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、TS枚举
// 给枚举变体一个起始数字值
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 {};。它其实与单元结构体一样,都表示一个类型。但是它不能被实例化。作用目前不详。
impl枚举
和 struct 一样 enum 同样可以 impl。
enum MyEnum {
Add,
Subtract,
}
impl MyEnum {
fn run(&self,x: i32, y: i32) -> i32 {
match self {
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语句来做分支管理。如果要定义一个新的类型,主要还是使用结构体。
match 表达式
match + 枚举
有点像 switch,用来匹配值是枚举的哪一个变体。
#[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::Triangle => {
println!("{:?}",Shape::Circle);
}
}
}
// 输出
Rectangle
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
let ret = match shape_a {就是比较地道的Rust写法,可以让代码显得更紧凑。
注意:match 表达式中各个分支返回值的类型必须相同。
_ 占位符
match 表达式中的所有分支都必须处理。否则会报错,但是难免会有有一些分支不想处理或者统一处理的情况,这是就用到 _ 占位符了。有点类似 switch 中的 default。
#[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);
}
更广泛的分支
match除了配合枚举进行分支管理外,还可以与其他基础类型结合进行分支分派。 下面是 The Book 中的示例。
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 实际上是模式匹配的入口。 模式匹配就是按对象值的结构进行匹配,并且可以取出符合模式的值。
模式匹配不限于 match,除了 match,rust 还给模式匹配提供了其他一些语法层面的设施。
if let
当要匹配的分支只有两个或者只想先处理一个分支的时候,就可以用 if let。
let shape_a = Shape::Rectangle;
match shape_a {
Shape::Rectangle => {
println!("1");
}
_ => {
println!("10");
}
};
用if let改写为:
let shape_a = Shape::Rectangle;
if let Shape::Rectangle = shape_a {
println!("1");
} else {
println!("10");
};
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 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);
}
匹配枚举
将变体中的结构体整体、元组各部分、结构体各字段解析出来的方式。
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
这个示例展示了如何将变体中的结构体整体、元组各部分、结构体各字段解析出来的方式。 用这种方式,我们可以在做分支处理的时候,顺便处理携带的信息,让代码变得相当紧凑而有意义(高内聚)。你需要熟悉并掌握这些写法,这样写起Rust代码来才会更加顺手。
匹配结构体
且看下面的例子:
#[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。
关于 partially moved我们在上节课已经讲过。
模式匹配过程中新定义的三个变量 name、age、student 分别得到了对应User实例a的三个字段值的所有权。
age 和 student 采用了复制所有权的形式,而 name 字符串值则是采用了移动所有权的形式。
a.name被部分移动到了新的变量 name ,所以接下来 a.name 就无法直接使用了。
这个示例说明 Rust 中的模式匹配是一种释放原对象的所有权的方式。
从Rust小助手的建议里我们看到了一个关键字:ref。
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 }
mut ref 获取目标的可变引用。
函数参数中的模式匹配
函数参数其实就是定义局部变量,因此模式匹配的能力在这里也能得到体现。
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 三个元组元素的内容,并在函数中使用。
#[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 语言从入门到实践这个小课。质量颇高!!