Rust 探索(八)—— 枚举与模式匹配(上)

1. 枚举类型

枚举类型的特点就是列举所有可能的值来定义一个类型

就比方说交通信号灯的颜色,总共有红、黄、绿3种可能,那么作为灯的颜色这个类型,有3个枚举变体,分别是RED(红)、YELLOW(黄)、GREEN(绿)

2. 定义枚举

使用enum关键字定义枚举类型

1
2
3
4
5
enum Signal {    // 使用enum定义枚举
RED,
YELLOW,
GREEN
}

如上所示,Signal就是枚举类的名称,对比着结构体的内容来看,RED、YELLOW、GREEN是3个成员,不过在枚举中,它们称为变体

另外,这三个变体同属于Signal这一个类型,红、黄、绿都是灯的颜色,只不过呈现的形式不同

3. 枚举值

1
2
let green = Signal::GREEN;
let red = Signal::RED;

枚举类和变体之间使用::分隔,这里的green和red相当于是对应枚举变体的实例了

image-20230530234608821

注意,这两个变量对应的是同一类型,正如Rust编译器所推导的一样

因此,如果需要定义函数处理它们,只需定义Signal类型的参数

1
2
3
fn decide(signal: Signal) {
// todo
}
1
2
decide(green);   // 同一处理变体
decide(red);

4. 枚举vs结构体

面对正确的场景使用正确的结构总是能发挥最大的优势

我们知道,红灯停、绿灯行、黄灯提醒,此时如果我们想要把灯的颜色和其代表的信息一同打包,应该如何选择呢?

如果使用结构体,那么可能会这样定义

1
2
3
4
struct SignalInfo {
color: Signal, // 灯的颜色枚举
info: String // 与之对应的含义
}
1
2
3
4
let green = SignalInfo {  // 以绿灯为例
color: Signal::GREEN,
info: String::from("行")
};

似乎没有很复杂,逻辑清晰,color字段接收颜色,info字段放提示信息,使用的时候实例化类型

但是,作为该领域的霸主,枚举举手表示它有更好的方案

1
2
3
4
5
enum Signal {
RED(String), // 直接关联类型
YELLOW(String),
GREEN(String)
}

这就好比结构体是跟别人借车放东西,而枚举是私家车,有东西直接打包放后备箱,明显更灵活

1
let green = Signal::GREEN(String::from("行"));

直观感受就是简短

并且,更加强大的一点是枚举后面关联的类型并不需要一样

1
2
3
4
5
enum Signal {
RED(bool),
YELLOW(String),
GREEN(i32)
}

私家车嘛,想怎么放是你的自由,用别人的车可是事先要说清楚的,临时变卦可不行

5. 枚举方法

枚举的方法定义其实和结构体非常相似,都是使用impl关键字

1
2
3
4
5
impl Signal {
fn show(&self) {
// 具体逻辑
}
}

同样地,使用impl创建一个与枚举同名的块,&self也是代表了实例

1
2
let red = Signal::RED(true);
red.show(); // 通过实例调用

相应地,如果定义关联函数,也是使用枚举名称加上::调用

1
2
3
4
5
impl Signal {
fn print() {
// 关联函数
}
}
1
Signal::print();

6. Option 枚举

Option枚举是Rust中一个很重要的存在,这种重要性需要从一个空值的概念说起

现代的许多编程语言都会有一个所谓的空值的概念,在Java中就是大名鼎鼎的null,并且很自然地就会联想到令人头疼的NullPointerException

我们通常的程序,都是需要有特定类型的值,用作书写业务逻辑,但是null表示一种无效的场景,用于和正确的逻辑赋值区别开来,可能是没有初始化或者传值不合法等等,这种null是不能直接拿来使用的,尤其是在其上调用函数,立马会导致程序崩溃

在代码达到一定规模时,这种可能出现的null会遍布程序当中,不知什么时候就会被触发,即使编写代码时,一直注意判断也很难保证安全,于是后来出现的一些语言针对空安全做了很多专项处理,比如说Kotlin、TypeScript等,Rust甚至索性不支持空值

不支持空值是指不会拿给你用,但是保留了一个概念,用以表示这种无效或缺失的情形

1
2
3
4
5
6
7
8
9
10
pub enum Option<T> {
/// No value.
#[lang = "None"]
#[stable(feature = "rust1", since = "1.0.0")]
None,
/// Some value of type `T`.
#[lang = "Some"]
#[stable(feature = "rust1", since = "1.0.0")]
Some(#[stable(feature = "rust1", since = "1.0.0")] T),
}

标准库中定义了Option枚举,它有两个枚举变体:NoneSome

None表示的就是所谓值无效或者缺失的含义,而Some表示一个T的类型,这里的T表示的泛型的概念,就如同Java的泛型和C++的模板,意味着Some可以表示各种类型的值,这也是正常情况下我们需要使用的有效值

1
2
let valid_value = Some("有效值");    // 直接赋值,推断出对应类型为字符串切片
let invalid_value: Option<i32> = None; // None必须显式声明类型

可以直接使用SomeNone,但是**None必须显式指定类型**,否则编译器无法推断

好像看起来,None和所谓null没什么区别,但是别忘了,SomeNone对应的可是同一个枚举类型

Option<T>表明T可能无效,也可能是T,这两种情况你必须都要考虑,不然你通不过编译,这就和Kotlin的可空类型差不多

通常的处理可以搭配上match表达式使用

7. 控制流运算符 match

1
2
3
4
5
6
enum Waste {
Recyclable, // 可回收垃圾
Other, // 其他垃圾
Kitchen, // 厨余垃圾
Hazardous // 有害垃圾
}
1
2
3
4
5
6
7
8
fn classify(waste: Waste) -> String {
match waste {
Waste::Recyclable => String::from("蓝色"),
Waste::Other => String::from("灰色"),
Waste::Kitchen => String::from("绿色"),
Waste::Hazardous => String::from("红色")
}
}

match后面跟一个表达式,便是上面的waste字段,这是带匹配的条件,接下来会依次与下面的各个变体进行匹配,这些候选的模式采用,分隔开来,并且使用=>将对应的值和代码逻辑关联起来,作为一个整体,类似于如果…那么…,与Java当中的开关语句switch...case接近

如果=>的代码有多行语句,那么还需要加上{}

1
2
3
4
5
6
7
8
9
10
11
fn classify(waste: Waste) -> String {
match waste {
Waste::Recyclable => String::from("蓝色"),
Waste::Other => { // 多行代码,使用{}
println!("仔细想想,不要乱扔!");
String::from("灰色") // 最后一句返回
},
Waste::Kitchen => String::from("绿色"),
Waste::Hazardous => String::from("红色")
}
}