05.枚举和模式匹配
一、枚举的定义
通过在代码中定义一个IpAddrKind
枚举来表现IP地址中的IPv4和IPv6。这被称为枚举的成员(variants):
enum IpAddrKind {
v4,
v6,
}
1、枚举值
fn main() {
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"),
};
}
枚举的成员位于其标识符的明明空间中,并使用两个冒号分开。这样设计的益处是现在IpAddrKind::V4
和IpAddrKind::V6
都是IpAddrKind
类型的。
这里我们定义了一个有两个字段的结构体 IpAddr:IpAddrKind
(之前定义的枚举)类型的 kind
字段和 String
类型 address
字段。我们有这个结构体的两个实例。第一个home
,它的 kind
的值是 IpAddrKind::V4
与之相关联的地址数据是 127.0.0.1
。第二个实例loopback
,kind
的值是 IpAddrKind
的另一个成员,V6
,关联的地址是 ::1
。
我将使用另一种更简洁的方式来表达,仅仅使用枚举并将数据直接放进每个枚举成员而不是将枚举作为结构体的一部分。
fn main() {
enum IpAddr {
V4(String),
V6(String),
}
let home = IpAddr::V4(String::from("127.0.0.1"));
let loopback = IpAddr::V6(String::from("::1"));
}
我们可通IpAddr
标准库进行IP地址的枚举操作。IpAddr in std::net - Rust (rust-lang.org)
虽然标准库中包含了一个IpAddr
的定义,仍然可以创建和使用我们自己定义而不会有冲突,因为我们并没有将标准库中的定义引入作用域。
2、Option枚举和其他相对于空值的优势
Option
是标准库定义的另一个枚举。Option
类型应用广泛因为它编码了一个非常普遍的场景,即一个值要么有值要么没值。Rust没有很多其他语言中有的空值功能。空值(Null)是一个值,它代表没有值。在有空值的语言中,变量重视这两种状态之一:空值和非空值。
Rust并没有空值,不过它确实拥有一个可以编码存在或不存在概念的枚举。这个枚举是Option<T>
,而且它定义于标准库中。
enum Option<T> {
None,
Some(T),
}
Option<T>
枚举已经被包含在prelude中,不需要再将其显式引入作用域。另外,它的成员也不需要Option::
前缀来直接使用Some
和None
。
<T>
语法是Rust功能,它是一个泛类型参数。目前你所需指导<T>
意味着Option
枚举的Some
成员可以包含任意类型的数据,同时每一个用于T
位置的具体类型使得Option<T>
整体作为不同的类型。
fn main() {
let some_number = Some(5);
let some_char = Some('e');
let absent_number: Option<i32> = None;
}
some_number
的类型是 Option<i32>
。some_char
的类型是 Option<char>
,这(与 some_number
)是一个不同的类型。因为我们在 Some
成员中指定了值,Rust 可以推断其类型。对于 absent_number
, Rust 需要我们指定 Option
整体的类型,因为编译器只通过 None
值无法推断出 Some
成员保存的值的类型。这里我们告诉 Rust 希望 absent_number
是 Option<i32>
类型的。
![[Pasted image 20221121202613.png]]
当有一个 Some
值时,我们就知道存在一个值,而这个值保存在 Some
中。当有个 None
值时,在某种意义上,它跟空值具有相同的意义:并没有一个有效的值。那么,Option<T>
为什么就比空值要好呢?
因为 Option<T>
和 T
(这里 T
可以是任何类型)是不同的类型,编译器不允许像一个肯定有效的值那样使用 Option<T>
。
二、控制流运算符match
Rust有一个叫做match
的极为强大的控制流运算符,它允许我们将一个值与一系列的模式相比较,并根据乡匹配的模式执行响应代码。match
的力量来源于模式的表现力以及编译器检查,它确保了所有可能的情况都得到处理。
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(conin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
fn main {}
函数value_in_cents
中的match
块,看上去与if
表达式十分相似,但是有一个很大的区别:在if
表达式中需要返回一个布尔值,而这里的表达式则可以返回任何类型。
match
分支由模式和它所关联的代码组成。第一个分支采用了值Coin::Penny
作为模式,并紧跟着一个=>
运算符用于将模式和代码区分开来。不同分支直接使用逗号分隔。当这个match
表达式执行时,它会将产生的结果值依次与每个分支中的模式相比较。如果匹配模式成功,则与该模式相关联的代码就会被继续执行;而假如模式匹配失败,则会继续执行下一个分支。
1、绑定值的模式
匹配分支另一个有趣的地方在于它们可以绑定被匹配对象的部分值。
#[derive(Debug)]
enum UsState {
Alabama,
Alaska,
// --snip--
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
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
}
}
}
fn main() {
value_in_cents(Coin::Quarter(UsState::Alaska));
}
我们在代码中调用value_in_cents(Coin::Quarter(UsState::Alaska))
,Coin::Quarter(UsState::Alaska)
就会作为coin
的值传入函数。这个值会一次与每个分支进行匹配,一直到Coin::Quarter(state)
模式才会终止匹配。这时,值UsState::Alaska
就会被绑定到state
上。接着,我们就可以println!
表达式中使用这个绑定了。
2、匹配Option<T>
fn main() {
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
}
let six = plus_one(five)
在调用plus_one(five)
时,plus_one
函数体中的变量x
被绑定为值Some(5)
。因为Some(5)
没有办法匹配上None
,语句会继续向下匹配,之后会匹配到Some(i)
。
let none = plus_one(None)
在运行时x
变成None
,一次进入match
表达式 ,并匹配到None
分支。
3、匹配必须穷举所有的可能
match
需要我们必须穷尽所有可能性,来确保代码是合法有效的。如果没有穷尽所有可能,这就会导致出现报错。
4、通配符(_
)
fn main() {
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
能让我们通过一种不那么烦琐的语法结合使用if
与let
,并处理那些只用关心某一匹配而忽略其他匹配的情况。
它匹配一个 config_max
变量中的 Option<u8>
值并只希望当值为 Some
成员时执行代码:
fn main() {
let config_max = Some(3u8);
match config_max {
Some(max) => println!("The maximum is configured to be {}", max),
_ => (),
}
}
为了满足 match
表达式(穷尽性)的要求,必须在处理完这唯一的成员后加上 _ => ()
,这样增加了代码可读性,因此使用if let
更为简单。
fn main() {
let config_max = Some(3u8);
if let Some(max) = config_max {
println!("The maximum is configured to be {}", max);
}
}
if let
语法使用一对以=
隔开的模式与表达式。它的工作方式与 match
相同,表达式对应match
中的输入,而模式则对应第一个分支。
match
和 if let
之间的选择依赖特定的环境以及增加简洁度和失去穷尽性检查的权衡取舍。