跳转到内容

Rust 枚举

枚举(enum,enumeration 的缩写)定义的类型,它的值只能是一组固定选项中的某一个。如果说结构体表达的是“这个东西同时拥有所有这些字段”,那么枚举表达的就是“这个东西恰好是这些选项中的一个”。

enum Direction {
North,
South,
East,
West,
}
fn main() {
let heading = Direction::North;
match heading {
Direction::North => println!("向北走"),
Direction::South => println!("向南走"),
Direction::East => println!("向东走"),
Direction::West => println!("向西走"),
}
}

输出:

向北走
▶ 动手试一试

注意,这里的 match 不需要 _ 来兜底:Direction 所有可能的值都已经列出来了,Rust 自己就能判断这个 match 是完整的。

这正是 Rust 的枚举比其他语言里常见的枚举更进一步的地方:每个变体都可以带上自己的数据。

enum Shape {
Circle(f64), // 半径
Rectangle(f64, f64), // 宽,高
}
fn area(shape: &Shape) -> f64 {
match shape {
Shape::Circle(radius) => 3.14159 * radius * radius,
Shape::Rectangle(width, height) => width * height,
}
}
fn main() {
let circle = Shape::Circle(3.0);
let rectangle = Shape::Rectangle(4.0, 5.0);
println!("{:.2}", area(&circle));
println!("{:.2}", area(&rectangle));
}

输出:

28.27
20.00
▶ 动手试一试

Shape::Circle 带一个 f64(半径),Shape::Rectangle 带两个。match 在每个分支里直接把这些数据取出来给你,不需要再单独“拆包”一次。

和结构体一样,枚举也可以有 impl 块,在里面写方法。方法内部通常用 match self 来决定每个变体该做什么:

#[derive(Debug)]
enum Light {
Red,
Yellow,
Green,
}
impl Light {
fn seconds(&self) -> u32 {
match self {
Light::Red => 30,
Light::Yellow => 3,
Light::Green => 25,
}
}
fn next(&self) -> Light {
match self {
Light::Red => Light::Green,
Light::Green => Light::Yellow,
Light::Yellow => Light::Red,
}
}
}
fn main() {
let mut light = Light::Red;
for _ in 0..4 {
println!("{:?} 灯亮 {} 秒", light, light.seconds());
light = light.next();
}
}

输出:

Red 灯亮 30 秒
Green 灯亮 25 秒
Yellow 灯亮 3 秒
Red 灯亮 30 秒
▶ 动手试一试

next 返回一个全新的 Light,light = light.next(); 再用它替换掉旧的值,所以 light 要声明成 mut。#[derive(Debug)] 用在枚举上和用在结构体上一样,{:?} 会打印出变体的名字。

把逻辑写成方法,可以让它紧挨着类型本身。用 Light 的人直接调用 light.seconds() 就行,不用关心时长是怎么定的。以后如果再加一个新变体,比如 FlashingYellow,编译器会把每一个需要更新的 match 都指出来。

Option 类型会在它自己的页面里正式介绍,其实它只是标准库里定义的一个枚举:

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

一旦理解了枚举,Option 就不再像什么特殊语法了:它只是一个再普通不过的枚举,有两个变体,其中一个刚好带着一个值。

为什么不用一个带“类型”字段的结构体?

Section titled “为什么不用一个带“类型”字段的结构体?”

你也许会想,用一个结构体加一个字符串字段(比如 "circle" 或 "rectangle")也能模拟出来。可这样一来,没有任何东西能阻止你手误写成 "circl",而且你还得自己去检查每种类型到底该用哪些字段。枚举从根本上让无效的组合写不出来,编译器也会逼着你处理每一个变体。

定义一个枚举 Coin,包含 Penny、Nickel、Dime 和 Quarter 四个变体。写一个 fn value_in_cents(coin: &Coin) -> u32,然后算出钱包里硬币的总额:一个 25 美分、一个 10 美分、一个 5 美分、一个 1 美分,再加一个 25 美分。提示:把硬币放进数组,再遍历 &purse。

查看答案
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 purse = [Coin::Quarter, Coin::Dime, Coin::Nickel, Coin::Penny, Coin::Quarter];
let mut total = 0;
for coin in &purse {
total += value_in_cents(coin);
}
println!("合计:{total} 美分");
}

输出:

合计:66 美分
▶ 动手试一试

拿本页的 Shape 枚举,加一个 Triangle 变体,存三条边的长度。然后写一个 perimeter 函数(周长,也就是绕图形一圈的长度),让它对每种图形都能用。提示:圆的周长是 2.0 * 3.14159 * radius,三角形的周长就是三条边相加。

查看答案
enum Shape {
Circle(f64),
Rectangle(f64, f64),
Triangle(f64, f64, f64),
}
fn perimeter(shape: &Shape) -> f64 {
match shape {
Shape::Circle(radius) => 2.0 * 3.14159 * radius,
Shape::Rectangle(width, height) => 2.0 * (width + height),
Shape::Triangle(a, b, c) => a + b + c,
}
}
fn main() {
let shapes = [
Shape::Circle(1.0),
Shape::Rectangle(4.0, 5.0),
Shape::Triangle(3.0, 4.0, 5.0),
];
for shape in &shapes {
println!("{:.2}", perimeter(shape));
}
}

输出:

6.28
18.00
12.00
▶ 动手试一试

试着把 match 里的 Shape::Triangle 分支删掉。Rust 会拒绝编译,因为这个 match 不再覆盖所有变体了。编译器就是这样确保你永远不会漏掉某种情况。

定义一个枚举 Temperature,它的两个变体 Celsius 和 Fahrenheit 各带一个 f64。加一个方法 to_celsius(&self) -> f64,然后把 Celsius(25.0)、Fahrenheit(212.0) 和 Fahrenheit(50.0) 换算成摄氏度打印出来,保留一位小数。提示:换算公式是 (f - 32.0) * 5.0 / 9.0。

查看答案
enum Temperature {
Celsius(f64),
Fahrenheit(f64),
}
impl Temperature {
fn to_celsius(&self) -> f64 {
match self {
Temperature::Celsius(c) => *c,
Temperature::Fahrenheit(f) => (f - 32.0) * 5.0 / 9.0,
}
}
}
fn main() {
let readings = [
Temperature::Celsius(25.0),
Temperature::Fahrenheit(212.0),
Temperature::Fahrenheit(50.0),
];
for t in &readings {
println!("{:.1} °C", t.to_celsius());
}
}

输出:

25.0 °C
100.0 °C
10.0 °C
▶ 动手试一试

因为 self 是借用来的,所以 c 和 f 都是引用(&f64)。数学运算可以直接作用在引用上,但要返回普通的摄氏度数值,就得写 *c,意思是“c 指向的那个数”。

  • 枚举把一个类型定义为几个具名变体中的一个:enum Direction { North, South, ... }。
  • 变体可以携带自己的数据,不同的变体可以携带不同的数据。
  • match 可以自然地解构枚举里的数据,并且要求处理每一个变体。
  • 和结构体一样,枚举也可以在 impl 块里定义方法。
  • Option(以及后面会讲到的 Result)只是标准库里的枚举,没有任何魔法。

小测验

1. 和其他语言里常见的枚举相比,Rust 枚举的每个变体能做到什么?

2. 为什么 Direction 的 match 示例不需要兜底的 _ 分支?

3. 在标准库里,Option<T> 实际上是用什么实现的?

得分: 0 / 3