Rust 枚举
枚举(enum,enumeration 的缩写)定义的类型,它的值只能是一组固定选项中的某一个。如果说结构体表达的是“这个东西同时拥有所有这些字段”,那么枚举表达的就是“这个东西恰好是这些选项中的一个”。
定义和使用枚举
Section titled “定义和使用枚举”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 是完整的。
枚举可以携带数据
Section titled “枚举可以携带数据”这正是 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.2720.00Shape::Circle 带一个 f64(半径),Shape::Rectangle 带两个。match 在每个分支里直接把这些数据取出来给你,不需要再单独“拆包”一次。
枚举也可以有方法
Section titled “枚举也可以有方法”和结构体一样,枚举也可以有 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:你早就在用的枚举
Section titled “Option:你早就在用的枚举”Option 类型会在它自己的页面里正式介绍,其实它只是标准库里定义的一个枚举:
enum Option<T> { Some(T), None,}一旦理解了枚举,Option 就不再像什么特殊语法了:它只是一个再普通不过的枚举,有两个变体,其中一个刚好带着一个值。
为什么不用一个带“类型”字段的结构体?
Section titled “为什么不用一个带“类型”字段的结构体?”你也许会想,用一个结构体加一个字符串字段(比如 "circle" 或 "rectangle")也能模拟出来。可这样一来,没有任何东西能阻止你手误写成 "circl",而且你还得自己去检查每种类型到底该用哪些字段。枚举从根本上让无效的组合写不出来,编译器也会逼着你处理每一个变体。
1. 数硬币
Section titled “1. 数硬币”定义一个枚举 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 美分2. 加上三角形
Section titled “2. 加上三角形”拿本页的 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.2818.0012.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 °C100.0 °C10.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
- 枚举的定义 —《Rust 程序设计语言》:带数据的变体、枚举上的方法,以及 Rust 为什么用
Option代替 null。 - match 控制流结构 —《Rust 程序设计语言》:深入讲解对枚举做 match,包含一个硬币分类的例子和通配模式。
- 枚举 — 通过例子学 Rust:可运行的示例,包括
Click { x: i64, y: i64 }这样带具名字段的变体。 - Enumerations — The Rust Reference(英文):完整的规则,比如怎样给变体指定明确的数值。
Option— 标准库文档(英文):你最常用的这个枚举的真正定义,以及它所有的辅助方法。