Rust 语言从入门到实战 唐刚--读书笔记06

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值