4. 我在外包学 rust——枚举、match、模式匹配

167 阅读10分钟

上节内容回顾

我在外包学 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 letwhile 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我们在上节课已经讲过。 模式匹配过程中新定义的三个变量 nameagestudent 分别得到了对应User实例a的三个字段值的所有权。 agestudent 采用了复制所有权的形式,而 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 语言从入门到实践这个小课。质量颇高!!