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 的!
模式匹配: 使用 match
和 if 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.
若有关联值的情况, 比如 Option
的 if 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
更易读, 写更少代码, 当然还需要根据实际需求自行选择使用哪个.