Rust 枚举和模式匹配

发布时间 2024-01-09 16:41:41作者: 二次元攻城狮


本文在原文基础上有删减,原文参考枚举和模式匹配

枚举的定义

结构体可以将字段和数据聚合在一起,而枚举可以将一个值成为一个集合之一。
定义一个 IpAddrKind 枚举:

enum IpAddrKind {
    V4,
    V6,
}

枚举值

创建 IpAddrKind 两个不同成员的实例:

let four = IpAddrKind::V4;
let six = IpAddrKind::V6;

注意:枚举的成员位于其标识符的命名空间中,并使用两个冒号分开

定义一个函数来获取任何 IpAddrKind,可以使用任一成员来调用这个函数:

fn route(ip_kind: IpAddrKind) {}
route(IpAddrKind::V4);
route(IpAddrKind::V6);

将数据直接放进每一个枚举成员

将 IP 地址的数据和 IpAddrKind 成员存储在一个 struct 中,关联枚举成员与值:

enum IpAddrKind {
    V4,
    V6,
}

struct IpAddr {
    kind: IpAddrKind,
    address: String,
}

let home = IpAddr {
    kind: IpAddrKind::V4,
    address: String::from("127.0.0.1"),
};

let loopback = IpAddr {
    kind: IpAddrKind::V6,
    address: String::from("::1"),
};

可以使用一种更简洁的方式来表达相同的概念,仅仅使用枚举并将数据直接放进每一个枚举成员而不是将枚举作为结构体的一部分。
IpAddr 枚举的新定义表明了 V4 和 V6 成员都关联了 String 值:

enum IpAddr {
    V4(String),
    V6(String),
}

let home = IpAddr::V4(String::from("127.0.0.1"));
let loopback = IpAddr::V6(String::from("::1"));

IpAddr::V4() 是一个获取 String 参数并返回 IpAddr 类型实例的函数调用,这些构造函数会自动被定义。

将不同类型和数量的数据放入枚举成员

用枚举替代结构体还有另一个优势:每个成员可以处理不同类型和数量的数据。枚举则可以轻易的处理这个情况:

enum IpAddr {
    V4(u8, u8, u8, u8),  //V4 地址存储为四个 u8 值
    V6(String),          //V6 地址存储为一个 String
}
let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));

存储和编码 IP 地址实在是太常见了,标准库提供了一个开箱即用的定义:

struct Ipv4Addr {
    // --snip--
}

struct Ipv6Addr {
    // --snip--
}

enum IpAddr {
    V4(Ipv4Addr),
    V6(Ipv6Addr),
}

这说明可以将任意类型的数据放入枚举成员中:例如字符串、数字类型或者结构体,甚至可以包含另一个枚举。

有关联值的枚举和结构体的相似性

一个 Message 枚举,其每个成员都存储了不同数量和类型的值:

enum Message {
    Quit,                       //Quit 没有关联任何数据
    Move { x: i32, y: i32 },    //Move 类似结构体包含命名字段
    Write(String),              //Write 包含单独一个 String
    ChangeColor(i32, i32, i32), //ChangeColor 包含三个 i32
}

如下结构体可以包含和上面枚举成员相同的数据,但它们都有不同的类型:

struct QuitMessage; // 类单元结构体
struct MoveMessage {
    x: i32,
    y: i32,
}
struct WriteMessage(String); // 元组结构体
struct ChangeColorMessage(i32, i32, i32); // 元组结构体

使用 impl 在枚举上定义方法

可以使用 impl 在枚举上定义方法,在 Message 枚举上定义一个叫做 call 的方法:

impl Message {
    fn call(&self) {
        // 在这里定义方法体
    }
}

let m = Message::Write(String::from("hello"));
m.call();

方法体使用了 self 来获取调用方法的值,上面的变量 m 就是当 m.call() 运行时 call 方法中的 self 的值。

Option 枚举和其相对于空值的优势

Option 是标准库定义的一个枚举,它编码了一个非常普遍的场景:一个值要么有值要么没值。

Rust 没有空值功能,空值(Null )是一个值,它代表没有值。在有空值的语言中,变量总是这两种状态之一:空值和非空值。

Rust 并没有空值,不过它确实拥有一个可以编码存在或不存在概念的枚举 Option<T>,而且它定义于标准库中,如下:

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

Option 枚举包含在 prelude 之中不需要将其显式引入作用域,它的成员也可以不需要 Option:: 前缀来直接使用 Some 和 None。

一些包含数字类型和字符串类型 Option 值的例子:

//根据Some 成员的值推断变量类型
let some_number = Some(5); //some_number 的类型是 Option<i32>
let some_char = Some('e'); //some_char 的类型是 Option<char>
//需要显示指定 Option 整体的类型 为 Option<i32>
let absent_number: Option<i32> = None;

因为 Option 和 T(这里 T 可以是任何类型)是不同的类型,编译器不允许像一个肯定有效的值那样使用 Option
代码尝试将 Option 与 i8 相加,无法通过编译:

let x: i8 = 5;
let y: Option<i8> = Some(5);
//错误,无法通过编译!
let sum = x + y;

在对 Option 进行运算之前必须将其转换为 T,这能帮助我们捕获到空值最常见的问题之一:假设某值不为空但实际上为空的情况。

为了使用 Option 值,需要编写处理每个成员的代码。match 表达式就是这么一个处理枚举的控制流结构:它会根据枚举的成员运行不同的代码,这些代码可以使用匹配到的值中的数据:

let some_value: Option<i32> = Some(42);
match some_value {
    Some(value) => {
        println!("The value is: {}", value);
        // 在这里可以使用 value
    }
    None => {
        println!("The value is None");
        // 处理 None 的情况
    }
}

match 控制流结构

Rust 有一个叫做 match 的极为强大的控制流运算符,它允许我们将一个值与一系列的模式相比较,并根据相匹配的模式执行相应代码。
注:模式可由字面值、变量、通配符和许多其他内容构成。

编写一个函数来获取一个未知的硬币,并以一种类似验钞机的方式,确定它是何种硬币并返回它的美分值:

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        //如果想要在分支中运行多行代码,可以使用大括号,而分支后的逗号是可选的
        Coin::Penny => {
            println!("Lucky penny!");
            1
        }
        //果分支代码较短的话通常不使用大括号
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

每个分支相关联的代码是一个表达式,而表达式的结果值将作为整个 match 表达式的返回值。

绑定值的模式

匹配分支的另一个有用的功能是可以绑定匹配的模式的部分值,这也就是如何从枚举成员中提取值的。

改变 Quarter 成员来包含一个 State 值:

#[derive(Debug)] // 这样可以立刻看到州的名称
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

在匹配 Coin::Quarter 成员的分支的模式中增加了一个叫做 state 的变量,当匹配到 Coin::Quarter 时变量 state 将会绑定对应州的值:

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter(state) => {
            println!("State quarter from {:?}!", state);
            25
        }
    }
}

匹配 Option<T>

在 Option<i32> 上使用 match 表达式的函数:

fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        None => None,
        //i 绑定了 Some 中包含的值
        Some(i) => Some(i + 1),
    }
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);

将 match 与枚举相结合在很多场景中都是有用的,Rust 代码中有很多这样的模式:match 一个枚举,绑定其中的值到一个变量,接着根据其值执行代码

匹配是穷尽的

以下代码没有处理 None 的情况,无法通过编译:

fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        Some(i) => Some(i + 1),
    }
}

Rust 中的匹配是 穷尽的(exhaustive):必须穷举到最后的可能性来使代码有效。

通配模式和 _ 占位符

对一些特定的值采取特殊操作,而对其他的值采取默认操作,模式 other 涵盖了所有其他可能的值:

let dice_roll = 9;
match dice_roll {
    3 => add_fancy_hat(),
    7 => remove_fancy_hat(),
    other => move_player(other),
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn move_player(num_spaces: u8) {}

当不想使用通配模式获取的值时,请使用 _ ,这是一个特殊的模式,可以匹配任意值而不绑定到该值:

let dice_roll = 9;
match dice_roll {
    3 => add_fancy_hat(),
    7 => remove_fancy_hat(),
    _ => reroll(),  //或者 _ => (),
}

fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn reroll() {}

if let 简洁控制流

可以认为 if let 是 match 的一个语法糖,它当值匹配某一模式时执行代码而忽略所有其他值。
match 只关心当值为 Some 时执行代码:

let config_max = Some(3u8);
match config_max {
    Some(max) => println!("The maximum is configured to be {}", max),
    _ => (),
}

可以使用 if let 这种更短的方式编写:

let config_max = Some(3u8);
if let Some(max) = config_max {
    println!("The maximum is configured to be {}", max);
}

可以在 if let 中包含一个 else。else 块中的代码与 match 表达式中的 _ 分支块中的代码相同,这样的 match 表达式就等同于 if let 和 else。
使用 match 表达式:

let mut count = 0;
match coin {
    Coin::Quarter(state) => println!("State quarter from {:?}!", state),
    _ => count += 1,
}

使用 if let 和 else 表达式:

let mut count = 0;
if let Coin::Quarter(state) = coin {
    println!("State quarter from {:?}!", state);
} else {
    count += 1;
}