Skip to main content

C6 Enum 和模式匹配

Enum 即枚举, 用于定义一个事物的多种变体, 比如月份的十二个月, 一个状态机的各种状态等.

Rust 中拥有强大的 Enum 支持, 包括内置的实用枚举类型 Option, Result 等, 以及 enum 的模式匹配.

定义 Enum

以 IP 地址为例, IP 地址目前有 v4 和 v6 两种:

enum IpAddrKind {
V4,
V6,
}

enum 的值按如下方式使用:

let ip_four = IpAddrKind::V4;
let ip_six = IpAddrKind::V6;

enum 可用于函数参数或返回值, 如下所示:

fn route(ip_kind: IpAddrKind) {}
fn ip_kind(addr: &str) -> IpAddrKind {}

和 Struct 结合使用:

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

// ...

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

使用上述方式会引入一个 struct, 更好的办法是使用带有 "关联值" 的 enum, 如下所示:

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

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

通过上面我们可以看到, 语法上: Enum 的名字也同时用作创建 enum 值.

当然 enum 中每个 case 可以有不同的关联值:

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

//...

let ip_four = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));

实际在 Rust 标准库中已经有 IPv4 和 IPv6 的定义, 由于地址上还有其他的数据, 因此它们类似如下:

struct Ipv4Addr {
// ...
}

struct Ipv6Addr {
// ...
}

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

**enum 中还可以关联许多不同的数据类型:

enum Message {
Quit, // 无关联值
Move { x: i32, y: i32 }, // 关联一个 "匿名 struct"
Write(String), // 关联一个有名字的 struct
ChangeColor(i32, i32, i32), // 关联三个 i32 类型
}

// 当然我们也可以将上述 enum 及其关联值每个都定义为一个结构体: 但这种方式的坏处是我们在定义处理这些类型的函数时会非常复杂.
// 如果使用 enum, 则函数输入就直接是 enum 类型, 而不是四个类型分别处理.

struct QuitMessage; // unit struct
struct MoveMessage {
x: i32,
y: i32,
}
struct WriteMessage(String); // tuple struct
struct ChangeColorMessage(i32, i32, i32) // tuple struct

Rust 标准类型 Option

Option 是 Rust 标准库中的类型, 表示 "有值" 或 "无值" 两种情况的 enum. 通过 Option 类型, Rust 中能够接触到 null 的地方非常少, 除非是在进行和 C/C++ 等语言的 FFI 时. (null 的发明被 Tony Hoare 称为是一个 "百亿美元失误", 即便它在语言实现时是非常容易的).

Option 在标准库中定义如下:

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

可以发现它是个泛型. 由于 Rust 是强类型语言, 通过 Option 可以完全避免其他语言中可能发生的和 null 发生运算的可能, 遇到 Option 均需要判断有无值:

let d: Option<i32> = Option::None;
let y = 3;

let sum = d + y; // 无法编译

我们不确定是否有值的时候, 必然就会使用 Option 类型来表示! 这样就完整避免了忽略对 null 判断的可能性(任何情况下要使用 Option 中的值时), 这样通过语言层面保证我们不会绕过对 null 的判断! 且任何只要不是使用 Option 的地方, 我们就能非常有信心地处理值, 因为我们可以确定值一定不是 null 的!

模式匹配: 使用 matchif let

可以通过模式匹配方便地对 enum 各个 case 进行处理.

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,
}
}

在模式匹配的每个 case 下都可以返回值, 通过大括号也可以添加额外的处理!

针对有关联值的 enum 模式匹配, 语法如下:

let some_num = Option::Some(3);

let res = match some_num {
Option::Some(n) => {
println!("value: {}", n);
n * 2
},
Option::None => {
println!("no value");
0
}
}

当然, 我们还可以在模式匹配时继续返回其他的 enum:

enum Result {
MoreThanOne,
Zero,
}

fn test_number(n: Option<i32>) -> Result {
match n {
Some(value) => {
if value > 1 {
Result::MoreThanOne
} else {
Result::Zero
}
},
None => Result::Zero
}
}

let one = Option::Some(1);
let res = test_number(one); // 结果为 Result::Zero

上述演示实际应用场景很多.

match 在使用时必须将所有 case 都进行处理, 但有时不想每个 case 都覆盖处理时, 可以使用 _ 进行 "Catch-all":

// 以之前的 Message 为例:
let msg = Message::Quit;

match msg {
Message::Quit => exit(0),
_ => println!("don't exit"),
}

如果不想做任何处理, 直接返回一个 empty tuple 即可: _ => (), :

let msg = Message::Quit;

match msg {
Message::Quit => exit(0),
_ => (),
}

上述还可以转换为使用 if let 达到目的:

let msg = Message::Quit;

if let Message::Quit = msg {
exit(0)
}

这样其他情况不进行任何处理, 且正确处理了 Quit 的 case.

若有关联值的情况, 比如 Optionif let 匹配, 也可以同样进行处理:

let some_num = Option::Some(3);

if let Opiton::Some(num) = some_num {
println!("value: {}", num);
} else {
println!("no value here");
}

match 对比下, if let 更易读, 写更少代码, 当然还需要根据实际需求自行选择使用哪个.