4

The Rust programming language 读书笔记——枚举类型

 3 years ago
source link: https://www.starky.ltd/2021/07/13/the-rust-programming-language-reading-notes-enum-and-match/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

The Rust programming language 读书笔记——枚举类型

发表于 2021-07-13

| 分类于 Program

|

| 阅读次数:

字数统计: 6.6k

|

阅读时长 ≈ 0:07

枚举类型(enum),通常也被简称为枚举,它允许我们列举所有可能的值来定义一个类型。
枚举搭配 match 表达式使用模式匹配,可以根据不同的枚举值来执行不同的代码。
Rust 中的枚举更类似于 Haskell 这类函数式编程语言中的代数数据类型(ADT)

假设我们需要对 IP 地址进行处理。目前只有两种广泛被使用的 IP 地址标准:IPv4 和 IPv6。
我们只需要处理这两种情形,且一个地址要么是 IPv4,要么是 IPv6,因此可以使用枚举将所有可能的值(IPv4 和 IPv6)列举出来,作为一种新的数据类型。

enum IpAddrKind {
V4,
V6,
}

现在,IpAddrKind 就是一个可以在代码中随处使用的自定义数据类型了。

可以参照下面的代码使用 IpAddrKind 中的两个变体(V4V6)创建实例:

let four = IpAddrKind::V4;
let six = IpAddrKind::V6;

由于 IpAddrKind:V4IpAddrKind:V6 拥有相同的类型(都是 IpAddrKind),我们可以定义一个接收 IpAddrKind 类型参数的函数来统一处理它们:

fn route(ip_type: IpAddrKind) { }

现在,我们可以使用任意一个变体来调用这个函数了:

route(IpAddrKind::V4);
route(IpAddrKind::V6);

当前定义的枚举类型 IpAddrKind,还只能区分 IP 地址的种类,没有办法去存储实际的 IP 地址数据。
可以使用结构体来解决这个问题:

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"),
};

实际上,我们可以直接将枚举关联的数据嵌入其变体内,而不用像上面那样将枚举集成至结构体中。

下面的代码直接定义了 IpAddr 枚举,V4V6 两个变体都被关联上了一个 String 值:

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

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

let loopback = IpAddr::V6(String::from("::1"));

我们直接将数据附加到枚举的每个变体中,就不需要额外地使用结构体了。

另外一个枚举替代结构体的优势在于,每个变体可以拥有不同类型和数量的关联数据,同时所有变体仍属于同一个枚举类型

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

let home = IpAddr::V4(127, 0, 0, 1);

let loopback = IpAddr::V6(String::from("::1"));

参考下面代码中定义的一个 Message 枚举:

enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}

该枚举拥有 4 个内嵌了不同类型数据的变体:

  • Quit 没有关联任何数据
  • Move 包含了一个匿名结构体
  • Write 包含了一个 String
  • ChangeColor 包含了 3 个 i32 值

枚举有些类似于定义多个不同类型的结构体。但枚举除了不会使用 struct 关键字,还将变体们组合到了同一个 Message 类型中。
下面代码中的结构体可以存储与这些变体完全一样的数据:

struct QuitMessage; // 空结构体
struct MoveMessage {
x: i32,
y: i32,
}
struct WriteMessage(String); // 元组结构体
struct ChangeColorMessage(i32, i32, i32); // 元组结构体

两种实现方式的差别在于,如果使用了不同的结构体,则每个结构体都会拥有自己的类型,无法轻易定义一个统一处理这些类型的函数。而前面的 Message 枚举是单独的一个类型

正如我们可以用 impl 关键字定义结构体的方法一样,我们同样可以为 Message 定义自己的方法:

impl Message {
fn call(&self) {
// 方法在这里定义
}
}

fn main() {
let m = Message::Write(String::from("hello"));
m.call();
}

Option 枚举及空值处理

Option 是一种定义于标准库中的枚举类型,它描述了一种值可能不存在的情形。借助类型系统,编译器可以自动检查我们是否妥善地处理了所有应该被处理的情况。

Rust 没有像其他语言一样支持空值(Null)。空值本身是一个值,但它的含义却是没有值。
空值的问题在于,当你尝试像使用非空值那样使用空值时,就会触发某种程度上的错误。由于空或非空的属性广泛散布在程序中,因此很难避免引起此类问题。
但空值本身所尝试表达的概念仍是有意义的,它代表了因为某种原因而变得无效或缺失的值。

Rust 中虽然没有空值,但提供了一个拥有类似概念的枚举 Option<T>,它可以用来标识一个值无效或缺失。
Option<T> 在标准库中的定义如下:

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

Option<T> 是一个普通的枚举类型,Some<T>None 是该类型的变体。

let some_number = Some(5);
let some_string = Some("a string");

let absent_number: Option<i32> = None;

若使用 None 而不是 Some 变体来进行赋值,则需要明确声明这个 Option<T> 的具体类型,否则编译器无法进行类型推导。

当我们有了一个 Some 值时,就可以确定值是存在的,并且被 Some 所持有;当我们有了一个 None 值时,就知道当前并不存在一个有效的值。
Option<T> 的设计相对于空值的优势在于,Option<T>T 是不同的类型,编译器不会允许我们像使用普通值一样直接去使用 Option<T> 的值。如:

let x: i8 = 5;
let y: Option<i8> = Some(5);

let sum = x + y;

运行上述代码会导致编译器报错,因为 i8Option<i8> 是不同的类型。
当我们持有的类型是 i8 时,编译器可以确保该值是有效的。但是当我们持有的类型是 Option<i8> 时,我们必须要考虑值不存在的情况,编译器会迫使我们在使用值之前正确地做出处理操作

为了持有一个可能为空的值,我们总是需要将其显式地放入对应类型的 Option<T> 值当中。当我们随后使用这个值时,也必须显式地处理它可能为空的情况
即在处理 Option<T> 时,必须编写应对每个变体的代码。某些代码只会在持有 Some(T) 值时运行,它们可以使用变体中存储的 T;另外一些代码则只会在持有 None 值时运行,这些代码没有可用的 T 值。

match 表达式就是一种可以用来处理 Option<T> 这类枚举的控制流结构。它允许我们基于枚举拥有的变体来决定运行的代码分支,并允许代码通过模式匹配来获取变体内的数据。

控制流运算符 match

match 是 Rust 中一个强大的控制流运算符,它允许将一个值与一系列模式相比较,并根据匹配的模式执行相应的代码。这些模式可以由字面量、变量名、通配符及许多其他东西组成。

下面的代码会接收一个美国的硬币作为输入,确定硬币的类型并返回其分值:

enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}

fn value_in_cents(coin: Coin) -> u32 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}

fn main() {
let coin = Coin::Dime;
println!("{}", value_in_cents(coin));
}

每个 match 分支所关联的代码同时也是一个表达式,这个表达式运行的结果同时也会作为整个 match 表达式的结果返回。

绑定值的模式

匹配分支还可以绑定匹配对象的部分值,这使得我们能够从枚举变体中提取特定的值。

比如美国的 25 美分硬币 50 个州采用了不同的设计。现在将这些信息添加至枚举中:

#[derive(Debug)] // 方便打印输出默认不支持打印的类型
enum UsState {
Alabama,
Alaska,
// ...
}

enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}

fn value_in_cents(coin: Coin) -> u32 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => {
println!("State quarter from {:?}.", state);
25
}
}
}

fn main() {
let alaska = UsState::Alaska;
let coin = Coin::Quarter(alaska);
value_in_cents(coin);
// => State quarter from Alaska.
}

上面的代码中,我们在模式中加入了一个名为 state 的变量用于匹配变体 Coin::Quarter 中的值。当匹配到 Coin::Quarter 时,变量 state 就会绑定到 25 美分所包含的值上。
比如代码中 Coin::Quarter(UsState::Alaska) 作为 coin 的值传入 value_in_cents 函数,最终值 UsState::Alaska 被绑定到变量 state 上。

匹配 Option

可以使用 match 表达式来处理 Option<T>,从 Some 中取出内部的 T 值。
比如编写一个接收 Option<i32> 的函数,若其中有值存在,则将这个值加 1;若其中不存在值,则直接返回 None

fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => {
println!("The result is None");
None
}
Some(i) => {
println!("The result is {}", i + 1);
Some(i + 1)
}
}
}

fn main() {
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
}

需要注意的是,匹配必须穷举所有的可能。尤其是 Option<T> 这个例子中,Rust 会强迫我们明确地处理值为 None 的情形。

简单控制流 if let

if let 能让我们通过一种不那么繁琐的语法结合使用 iflet,处理那些只关心某一种匹配而忽略其他匹配的情况。
下面的代码会匹配一个 Option<u32> 的值,并只在值为 3 时执行代码:

fn main() {
let some_number = Some(3);
match some_number {
Some(3) => println!("three"),
_ => (),
}
}

为了满足 match 表达式穷尽性的要求,我们不得不在处理完 Some(3) 变体后额外加上一句 _ => ()
可以使用 if let 以一种更简单的方式实现上述代码:

if let Some(3) = some_number {
println!("three");
}

还可以在 if let 中搭配使用 else

fn main() {
let some_number = Some(8);
if let Some(3) = some_number {
println!("three");
} else {
println!("other number");
}
}

The Rust Programming Language


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK