菜鸡前端的Rust学习笔记(六)—枚举和模式匹配

987 阅读10分钟

写在前面

在本章中我们将看到枚举类(enum),本章内容主要分成几个部分:

  1. 定义和使用枚举值,来解决代码中的"魔法数字"的问题
  2. 了解Option这种特殊的枚举
  3. 使用match表达式进行模式匹配
  4. 使用if let结构,作为另一种方便、简洁可用的方案处理代码中的枚举值

6.1 定义枚举

干啥用的:枚举就是我们将一个变量有所有可能的值都列举出来的一种方式,这种方式增加了代码的语义化,也能让代码中的魔法数字减少,何乐而不为呢。文中讲了一个IP的事情,就是我们可能遇到IP的类型有IPV4和IPV6,所以是可以穷尽的。

定义方法:通过enum来定义

enum EngineCylinder {
    V4,
    V6,
    V8,
    V12,
}

6.1.1 枚举值的使用

使用方法:使用::语法来调用对应的类型

fn main() {
    // 这里我们就得到了两个枚举的实例
    let v6Engine = EngineCylinder::V6;
    let v8Engine = EngineCylinder::V8;
}

注意点

  1. 枚举是一个类似type的概念,他只是告诉你,可能这个数据当前是什么状态,或者是什么类型,并不代表其本身是什么数据。
  2. 在类型和函数参数的定义上,这里的v6Enginev8Engine其实它对应的类型都是枚举EngineCylinderv6v8只是他对应的实例的值而已,所以其实枚举类就是一个单独的类型,其可以作为函数的参数定义,也可以作为struct中一个键值对应的类型。
  3. struct类似,如果要println打印对应的值的话,需要derive[Debug]

例子:

这里我们为Engine定义了三个字段:功率power,气缸数cylinderNumber,排量cc和发动机名称name(对应的值我乱写的)

#[derive(Debug)]
enum EngineCylinder {
    V4,
    V6,
    V8,
    V12,
}

struct Engine {
    cylinderNumber: EngineCylinder,
    power: u32,
    cc: u32,
    name: String
}

fn main() {
    let v6Engine = EngineCylinder::V6;
    let v8Engine = EngineCylinder::V8;
    
    checkEngine(v6Engine);
    checkEngine(v8Engine);
    
    let engine = Engine {
        cylinderNumber: EngineCylinder::V6,
        power: 261,
        name: String::from("3.0T"),
        cc: 2956
    };

}

fn checkEngine(engine: EngineCylinder) {
    println!("engine is -> {:?}", engine)
}

在定义枚举的时候,如果枚举的值写的太短,可能会让枚举的语义化也不太清楚,如果写的太长,可能导致枚举本身用起来太麻烦,因此,可以通过在枚举的时候传入参数的方式,来让枚举值更加的清晰,直接让枚举值和对应的值相关联。

我们来看下面这个例子,目前有三个汽车品牌,吉利、领克、蔚来,他们各自有不同的车型,这里的领克05和领克03,通过跟在枚举值后面的String就可以区分出,具体是哪个车型,非常好用,将一个枚举进行了扩展。枚举值是和后面的字符串相对应的

在这个枚举后面的类型也可以是自己定义的结构体,他的作用,就是让你将某个枚举和某个值关联起来,当然这个地方,所有定义的枚举值,其变种都可以是不一样的

#[derive(Debug)]
enum CarBrand {
    Geely(String),
    LINCO(String),
    NIO(String)
}

enum Message {
    // 没有任何数据关联
    Quit,
    // 命名了一个结构体类型
    Move { x: i32, y: i32 },
    // 关联了一个String类型
    Write(String),
    // 关联了一个包含3个i32的元组结构
    ChangeColor(i32, i32, i32),
}

fn main() {
    let geelyIcon = CarBrand::Geely(String::from("Icon"));
    let linco05 = CarBrand::LINCO(String::from("05"));
    let linco01 = CarBrand::LINCO(String::from("01"));

    println!("05 -> {:?}, 01 -> {:?}", linco05, linco01);
}

另一种玩法:定义的枚举类型可以关联各种不同类型的数据,下面的Message和定义多个struct效果上是相同的,但是唯一的区别就是,对于这样定义的枚举来说,其对应的上层的类型都是Message,而如果你定义了多个结构体类型,那他们都是不同的结构体。和struct类似,利用impl方法也可以为枚举中特殊的类,定义对应的方法。

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

impl Message {
    fn invoke (&self) {
        println!("123")
    }
}

fn main () {
    let write_message = Message::Write(String::from("123"));
    write_message.invoke(); // 123
}

6.1.2 Option枚举和他超越Null的优势

Option类型被用于很多场景,因为一个值可能是一些类型(就是由外部决定),也可能啥也不是。Option类型目前就是为了处理一些通用场景设计的。这种数据解构的意义在于告诉编辑器你是否是合理处理了所有的场景,避免在其他编程语言中很通用场景下的一些bug

目前Rust不像其它语言一样,目前是没有Null这个数据类型的,因为文中认为其实把Null赋值给非Null得元素,其本身就是一个错误,没有存在得意义,那么问题来如果我们需要给默认值,一个什么都没有得值,我们需要怎么去表示呢?

enum Option<T> {
    None,
    Some(T)
}

fn main () {
    let some_number = Some(5);
    let some_string = Some("a string");
    let absent_number: Option<i32> = None;
}

注意点

  1. 使用Option得目的是为了模拟,一个值可能会有也可能会没有得这个场景
  2. 使用SomeNone可以不使用Option::,在rust中会自动映射到std::options::Option
  3. Some具有类型推断得作用,会根据Some中传入得元素,自动推断对应得类型
  4. <T>语法是一个泛型类型,和其他语言中差不多,后面会细讲得
  5. Rust需要我们对所有得参数进行推断,比如let absent_number: Option<i32> = None;,我们如果仅仅右边是None得话,其实rust还是不知道你这个类型到底是啥,需要人为定义为Option<i32>

使用Option的值

如下面的例子,使用Option之后,因为你的值除了是i32还有可能是None所以编译器就认为你这个地方如果是Nonei32相加,类型上是有问题的会报错,虽然讲了很多原理,但是也没有讲具体如何解决,只是说让你看文档Rust Option 文档,我们继续往下看吧,理论上判断出类型之后,用match关键字来决定接下来要做什么,我们下一章来看match

fn main () {
    let x: i32 = 5;
    let some_number: Option<i32> = Some(5);
    let some_string = Some("a string");
    let absent_number: Option<i32> = None;

    // 这里会报错,因为i32和Option<i32>不能相加
    let rlt = x + some_number;
}

6.2 match控制流操作符

6.2.1 match语法的写法

在Rust中有一个功能很强的match控制流操作符,他允许你对一个值进行一系列的比较,然后基于比较结果,执行一系列的操作。

这种模式对比可以是对比结构体的值,变量名,通配符和其他各种东西。

文中的例子是根据硬币的大小材质来分辨硬币的面额,这里我们用一个根据地名猜城市的例子,来举例可能会更加生动:

#[derive(Debug)]
enum ScenicSpot {
    // 长城
    GreatWall,
    /// 西湖
    WestLake,
    // 乐山大佛
    LeshanGiantBuddha,
}

fn main() {
    let spot = ScenicSpot::WestLake;
    let city = get_spot_city(&spot);

    println!("city -> {}", city);
}

fn get_spot_city(spot: &ScenicSpot) -> String {
    match spot {
        ScenicSpot::GreatWall => String::from("北京"),
        ScenicSpot::LeshanGiantBuddha => {
            String::from("四川")
        },
        ScenicSpot::WestLake => String::from("杭州"),
    }
}

注意点

  1. get_spot_city这个方法中,我们通过match关键字 + 需要比较的变量 + {}语法来规定比较的操作
  2. 不同的比较之间,通过,来分割,如果每个判断中有多行代码,可以用{}来进行多部操作,包一下,其实这里就是一个函数
  3. 只有match命中了那一条规则,才会进入到后续的函数体中

6.2.2 对绑定了对应值的枚举进行匹配

之前讲过的就是枚举值,其实可以通过给一个值,来做细分的,那么这种类型的枚举值要如何通过match语法来细分呢,这里match可以是一个函数,其参数接受的值,就是现在这个value在当前判断条件下的值,所以可以写一段逻辑判断具体的如下:

#[derive(Debug)]
struct Car {
    brand: String,
    version: String, 
    max_speed: u32,
    name: String
}
​
#[derive(Debug)]
struct Airplane {
    company: String,
    max_speed: u32,
    price: u32
}
​
#[derive(Debug)]
enum Vehicle {
    Walk,
    LandTraffic(Car),
    AirTraffic(Airplane)
}
​
fn get_traffic_tool(tool: &Vehicle) -> String {
    match tool {
        Vehicle::AirTraffic(state) => {
            println!("空运公司为 -> {}", state.company);
            String::from("坐飞机啦~")
        },
        Vehicle::LandTraffic(state) => {
            println!("今天坐 -> {}", state.name);
            String::from("坐车啦~")
        },
        Vehicle::Walk => String::from("走路锻炼身体~也不错")
    }
}
​

6.2.3 匹配Option

之前在8.1中讲Option的时候,我们说因为Option<T>中可能包含None所以无法和对应的确定的u32类型相加,这里提供过match可以判断出当他不是None的时候来进行相加,我们看一个例子

fn main () {
    let x: i32 = 5;
    let some_number: Option<i32> = Some(5);
    let some_string = Some("a string");
    let absent_number: Option<i32> = None;
​
    let rlt: Option<i32> = match some_number {
        None => None,
        Some(state) => Some(state + x)
    };
​
    println!("rlt -> {:?}", rlt);
}

注意点

  1. 操作后的值类型,这里我们定义的也为Option,所以需要通过Option包一下
  2. 为什么Some(5)会命中Some规则,因为他们都涉及同一个变体,只不过他们得值不同,所以能够匹配上
  3. None的值也要判断,如果match没有对值得所有可能做判断得话,会报错(尽可能充分得进行匹配)

通过match + enum在很多场景下是非常有用得,你将会看到

6.2.4 捕获所有类型和_占位符

use rand::Rng;
​
fn match_number() -> String {
    let rand_num = rand::thread_rng().gen_range(1..10);
    
    let rlt_message = match rand_num {
        5 => String::from("二等奖"),
        3 => String::from("一等奖"),
        _ => String::from("你没中奖")
    };
​
    rlt_message
}
​
fn main() {
    println!("game res -> {}", match_number());
}

注意点

  1. 前两行代码用来指出了3和5匹配上得条件,后面得第三个条件用来匹配其他剩下得所有可能
  2. 一般都是通过最后的东西来作为兜底

6.3 用if let实现简洁的控制流

使用if let这个语法可以让你简单处理就是match场景下,只需要一次匹配的场景,而忽略其余剩下的所有可能,比如以我们刚才相加的例子为例:

fn main () {
    let x: i32 = 5;
    let some_number: Option<i32> = Some(5);
    let some_string = Some("a string");
    let absent_number: Option<i32> = None;
​
    // 这里我们需要同时处理None和Some这两种匹配上的情况
    let rlt: Option<i32> = match some_number {
        None => None,
        Some(state) => Some(state + x)
    };
​
    println!("rlt -> {:?}", rlt);
}

如果使用if let语法我们只需要处理其中一个我们需要的场景即可,这里我们不需要处理None的条件

fn main() {
    let x = None;
    let y = Some(5);
    get_add_result(&x);  // 数字是无效的
    get_add_result(&y); // 输入的值加6后是 -> 11
}
​
fn get_add_result (x: &Option<u32>) {
    if let Some(state) = x {
        let s = 6;
        println!("输入的值加6后是 -> {}", s + state);
    } else {
        println!("数字是无效的")
    }
}

注意点

  1. if let这种语法其实用法上你理解就是和if一样,并不能像match一样直接有返回值
  2. else类似之前的_用于处理除了选中的那一种场景外的所有场景

6.4 总结

这一张哦我们展示了各种枚举值enum的创建、Option<T>类型的创建,当我们需要对枚举值做出判断时match的用法和if let的精简写法。现在在你的Rust程序中可以通过结构体和枚举来表达各种观念,通过创建你自己的类型来保证API的类型安全。编译器将能确保你的函数能够得到你想要的值。为了能让你更好的设计出好用的API,接下来我们将进入Rust的模块系统。