【Rust 学习记录】6. 枚举与模式匹配

TwoSix Lv3

定义枚举

例子:IP地址,只有 IPV4, IPV6 两个模式。

所以我们可以通过枚举类型的方式来描述 IP 地址的类型。

1
2
3
4
5
6
7
8
9
enum IpAddKind{
IPV4,
IPV6
}

fn main() {
let four = IpAddKind::IPV4;
let six = IpAddKind::IPV6;
}

这里也就两个点:

  1. 使用 enum 关键字就可以完成一个枚举类型的定义。
  2. 访问枚举类型的值是通过命名空间的方式来实现的,而不是点运算符。

接下来再谈论一个实际的问题:这里的枚举类型只定义了两个类别,没有办法对应实际的IP地址,怎么办?

一般情况下,我们首先想到的肯定是用结构体,一个字段存储类型,一个字段存储地址对吧。但是 Rust 有一个非常方便的特性:关联的数据可以直接嵌入到枚举类型里

1
2
3
4
5
6
7
8
9
enum IpAddKind{
IPV4(String),
IPV6(String)
}

fn main() {
let local = IpAddKind::IPV4(String::from("127.0.0.1"));
let loopback = IpAddKind::IPV6(String::from("::1"));
}

厉害吧。不过好像结构体也能做是吧?不完全是,枚举类型有一个结构体做不到的功能。

枚举类型可以为每个类型值指定一个类型,例如 IPV4 通常是四个数字来表示,而 IPV6 则不同,那么我们可以使用四个数字来描述 IPV4 ,使用字符串来描述 IPV6

1
2
3
4
enum IpAddKind{
IPV4(u8, u8, u8, u8),
IPV6(String)
}

这个时候,把类型和内容拆分成两个字段来描述的结构体,就没有办法实现了,只能固定一种类型。

另外,IPV4和IPV6因为很常用,所以在标准库里其实就有定义好了一套枚举类型。它的定义方法是这样的。

img

也就是先用两个结构体描述一下具体的 IPV4和IPV6 ,然后再定义一个枚举类型,把这两个结构体嵌入到枚举类型里。

另外,枚举类型还有对于结构体另外的优势是,我们可以轻松的定义一个用于处理多个数据类型的函数

例如,我们可以像上面官方一样,用两个结构体去描述 IPV4和IPV6,但如果我们要定义一个函数,同时处理IP地址的话,就不知道该指定传入参数的类型是 IPV4还是IPV6了,但我们定义一个枚举类型 IpAddr ,就可以轻松的定义函数的传入类型为 IpAddr,然后可以同时处理两个结构体的数据。

Option——一个常用的枚举类型

Option 枚举类型定义了值可能不存在的情况,或者可以说是其他语言的空值 Null 。本来就很常用了,但这个类型在 Rust 里更有用,因为编译器可以自动检查我们是不是妥善的处理了所有应该被处理的情况

Rust 并没有像其他语言一样,有空值 Null 或者 None 的概念。书上说这是个错误的设计方法,因为当你定义了一个空值,在后续可能没有给他赋予其他值就进行使用的话,就会触发问题。这种设计理念可以帮助人们更方便的去实现一套系统,但也给系统埋下了更多的隐患。

Rust 结合了一下这个理念,觉得空值是有意义的,触发空值问题的本质是实现的措施的问题,所以提供了一个具有类似概念的枚举类型——Option. 标准库里的定义是这样的

1
2
3
4
enum Option<T>{
Some(T),
None,
}

Option 是被预导入的,因为它很有用,所以我们不用 use 来导入它。所以我们可以不用指定命名空间,使用 Some 或者 None 关键词, 是后面学的语法,用来代表任意类型

我们可以用这个方法来定义一些变量

1
2
3
4
5
fn main() {
let some_number = Some(5);
let some_string = Some("hello");
let absent_number = None;
}

这段编译是不通过的,这也体现了 Option 相对于普通空值的优势。

我们来简单的通过几个例子来了解一下 Option 的设计理念

  1. Option和 T 是两个不同的类型
1
2
3
4
5
fn main() {
let a = Some(5);
let b = 5;
println!("{}", a+b);
}

可以看到这段代码里,虽然 a, b 同样都是 i32 但一个是被Some持有,那它们就是不同的类型,编译器无法理解不同类型相加,所以这就意味着什么。当我们对于一个可能有值,可能没值的变量,我们就要去使用 Option 枚举,一旦使用了Option枚举,我们再要实际使用它的时候,就要显式的把这个 Some 类型的变量转换到 i32 的变量,再去使用。相当于强迫你去对这个变量编写一段代码,这样就避免了你不经过处理就使用,结果使用到空值的情况。

当然,因为是枚举类型,我们也可以方便的用 match 来处理不同的情况,例如有值时怎么样,没值时怎么样等等。接下来就开始介绍一下 match

match运算符

match 是一个很好用的运算符,它除了提高可读性外,也方便编译器对你的代码进行安全检查,确保你对所有可能的情况都进行了处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
enum Coin{
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}

fn main() {
let my_coin = Coin::Dime;
println!("The value of my coin is {} cents", value_in_cents(my_coin));
}

这就是一个简单的,输入硬币类型,返回硬币价值的代码。相对于 if-else 表达式,match 的第一个优势自然就是可读性。你 if-else 需要想方设法凑一个 bool 值,可能用字符串,可能用字典,可能用数字下标,可能用结构体什么的,但 match 枚举类型就不用,可以为任意你想要的类型定义一个名字,直接用,直接返回任意值,结构还更紧凑好看。

另一个优势就是,match 运算符可以匹配枚举变量的部分值。

美国的25美分硬币里,很多个州都有不同的设计,也只有25美分的硬币有这个特点,所以我们可以给25美分加一个值:州,对应不同的设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#[derive(Debug)]
enum UsState{
Alabama,
Alaska,
}

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!("The state of coin is {:?}", state);
25
}
}
}

fn main() {
let my_coin = Coin::Quarter(UsState::Alaska);
println!("The value of my coin is {} cents", value_in_cents(my_coin));
}

这里我们用到了之前的 Debug 注解,让编译器自动格式化枚举类型的输出,然后在match匹配里,提取了 Quarter 枚举类型里的值,命名为 state,然后输出。

匹配Option

接下来我们就可以综合一下上面学到的东西,用match来处理一下空和非空的情况了。

1
2
3
4
5
6
7
8
9
10
11
fn pluse_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}

fn main() {
let num = Some(5);
let num2 = pluse_one(None);
}

这就是一个典型的例子:

  1. 当为空的时候,什么也不干
  2. 当不为空的时候,取出值,进行处理

这也就是 match 的相对于 if-else 的优势:可以以一个更简单,更紧凑,可读性更高的方式,进行模式匹配,进行对应值的处理。接下来就介绍 match 在安全性方面的优势:让编译器帮你检查是否处理完了所有情况。

必须穷举所有可能

我们来试着漏处理为空值的情况。

1
2
3
4
5
6
7
8
9
fn pluse_one(x: Option<i32>) -> Option<i32> {
match x {
Some(i) => Some(i + 1),
}
}

fn main() {
let num = Some(5);
}

编译器马上报错:non-exhaustive patterns: None not covered 你没有覆盖处理空值!

这一段代码同时体现了 match 的优势和 Option 实现空值的优势。也就是你必须要处理所有情况,保证没有一点逻辑遗漏的地方,Option也依赖于 match 的这个特性,强迫你处理空值的情况,杜绝了大部分程序员只写一部分 if-else 结果就因为漏了部分情况没处理,导致程序 crash 的问题。

_ 通配符

但有时候我明明不用处理所有情况,但每次都要写上很麻烦怎么办?没事,Rust 作为一个现代语言还是准备了一些语法糖,那就是通配符 _ 相当于 if-else 里面的 else。

1
2
3
4
5
6
7
8
9
10
fn pluse_one(x: Option<i32>) -> Option<i32> {
match x {
Some(i) => Some(i + 1),
_ => None
}
}

fn main() {
let num = Some(5);
}

_ 可以匹配所有类型,所以得放最后,用于处理前面没有被处理过的情况。

但有时候我指向对一种情况处理怎么办?还是有点麻烦吧,Rust 还准备了 if let 语句

简单控制流 if-let

我们就继续用美分硬币来举例吧,之前写的代码方便些。例如我们只想知道这个硬币是不是 Penny。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
enum Coin{
Penny,
Nickel,
Dime,
Quarter,
}

fn main() {
let my_coin = Coin::Penny;
match my_coin{
Coin::Penny => println!("Lucky Penny!"),
_ => println!("Not a penny"),
};
}

用 match 的话,就是要定义一个硬币,匹配这个硬币,再写个通配符来检测其他情况,标准流程是吧。但这样写或许繁琐了些,if let 就是这么一个一步到位的语法

1
2
3
4
5
6
7
8
9
10
11
12
13
enum Coin{
Penny,
Nickel,
Dime,
Quarter,
}

fn main() {
let my_coin = Coin::Penny;
if let Coin::Penny = my_coin {
println!("Lucky penny!");
}
}

if let Coin::Penny 就是要匹配的值,my_coin也就是你传入的值,用等号分隔开,如果两者相等的话,执行花括号内的语句。

当然,也可以用else

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
enum Coin{
Penny,
Nickel,
Dime,
Quarter,
}

fn main() {
let my_coin = Coin::Penny;
if let Coin::Penny = my_coin {
println!("Lucky penny!");
}else{
println!("Not a lucky penny!");
}
}

这样就和上面的 match 完全匹配了。

虽然 if let 让代码写起来更简单了,但也失去了 match 的安全检查,所以是一个取舍吧。个人偏向于使用match,说实话我觉得这 if let 写起来有点别扭,感觉书写逻辑不太舒服。

这应该只是一个偶尔可能用到的糖,哪时候你写烦了match,可以想想原来还有 If let 这么个东西


第六章也就搞定了,大致总结一下,这章主要就是讲了一下1. 枚举类型对于结构体的优势所在,2. match对于if-else的优势所在,3. 独特的空值设计理念

今天就到这里吧,后面还有7,8,9章三章的基础通用编程知识,学完就差不多进入进阶阶段了。

  • 标题: 【Rust 学习记录】6. 枚举与模式匹配
  • 作者: TwoSix
  • 创建于 : 2023-03-30 22:38:03
  • 更新于 : 2024-07-04 23:52:28
  • 链接: https://twosix.page/2023/03/30/【Rust-学习记录】6-枚举与模式匹配/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论