枚举与 Option
你会学到什么
- 枚举的每个变体可以携带不同的数据(结构体式字段或元组式字段)。
- 给枚举写
impl方法,在方法里match self。 - 用
match对枚举做穷尽处理。 Option<T>是标准库枚举,替代其他语言的null。Option的常用组合子map/and_then/unwrap_or/unwrap_or_else,以及if let、let ... else。
最小示例
enum Shape {
Circle { radius: f64 }, // 结构体式变体
Rectangle { width: f64, height: f64 },
Triangle(f64, f64), // 元组式变体
}
示例里还有一个更贴近业务的 Event 枚举(点击、按键、粘贴、关闭),演示枚举如何统一表达“一组互斥的消息”。
运行代码
cd examples
cargo run -p rt_11_enums_option
cargo test -p rt_11_enums_option
代码讲解
枚举 + impl 方法
把行为挂在枚举上:area 是 Shape 的方法,内部 match self。match 必须覆盖所有变体,新增 Triangle 后若忘了补分支,编译器会直接报错——这正是枚举安全的来源。name 方法用 .. 忽略不关心的字段,只判断是哪个变体。Event 枚举的 describe 同理,把四种事件渲染成一行日志,展示枚举如何统一表达“一组互斥的消息”。
Option 与组合子
Option<T> 只有两个变体:Some(T) 和 None。Rust 没有 null,“可能没有值”被编码进类型,迫使你显式处理。除了最完整的 match,示例里还演示了几种更简洁的写法:
if let Some(n) = ...:只关心Some分支时的简写。let Some(first) = ... else { return; }:拿不到值就提前退出,让主流程保持平铺、不嵌套。map:在Some内部做变换,None原样传递(first_even_squared_or_zero里map(|n| n * n))。and_then:上一步是Some才继续,否则短路(parse_positive先parse().ok()再and_then过滤正数)。unwrap_or:取值或给一个固定默认值。unwrap_or_else:默认值需要计算时用闭包,只有None时才执行,避免无谓开销。
常见错误
对 Option 直接 unwrap() 而不处理 None:
let value = first_even(&[1, 3, 5]).unwrap(); // ❌ None 时 panic
更安全的做法是 match、if let、let ... else,或带默认值的 unwrap_or / unwrap_or_else。
练习
- 给
Shape再加一个Square { side }变体,并补全area与name。 - 给
Event加一个Scroll { delta: i32 }变体,并补全describe。 - 写一个返回
Option<&str>的函数取出切片首元素,再用map把它转成大写。
小结
枚举建模“多选一”的数据,match 保证穷尽处理,Option 用类型消灭空指针。
下一步
match 的能力远不止区分变体。下一章系统学习模式匹配。
完整示例代码
下面是 examples/11_enums_option/ 的完整源码。无需 clone 仓库,直接在页面上阅读、复制、对照运行。
examples/11_enums_option/src/main.rs
//! 枚举与 Option:用带数据的枚举建模“多选一”,用 `Option<T>` 表达“可能没有值”。
//!
//! 本示例覆盖:
//! - 带数据的变体(结构体式与元组式)。
//! - 为枚举实现 `impl` 方法,并在方法里 `match self`。
//! - `Option<T>` 的常见组合子:`map` / `and_then` / `unwrap_or` / `unwrap_or_else`。
//! - `if let` 与 `let ... else` 两种简写。
/// 几何图形:每个变体携带各自需要的字段。
#[derive(Debug, PartialEq)]
enum Shape {
/// 圆,由半径决定。
Circle { radius: f64 },
/// 矩形,由宽高决定。
Rectangle { width: f64, height: f64 },
/// 三角形(元组式变体):底、高。
Triangle(f64, f64),
}
impl Shape {
/// 计算面积——方法内部 `match self`,编译器保证覆盖所有变体。
fn area(&self) -> f64 {
match self {
Shape::Circle { radius } => std::f64::consts::PI * radius * radius,
Shape::Rectangle { width, height } => width * height,
Shape::Triangle(base, height) => 0.5 * base * height,
}
}
/// 返回变体的中文名字,演示对 `self` 的只读匹配。
fn name(&self) -> &'static str {
match self {
Shape::Circle { .. } => "圆",
Shape::Rectangle { .. } => "矩形",
Shape::Triangle(..) => "三角形",
}
}
}
/// 一个更贴近业务的枚举:用户界面事件。
#[derive(Debug)]
enum Event {
/// 点击坐标。
Click { x: i32, y: i32 },
/// 按下某个键。
KeyPress(char),
/// 粘贴一段文本。
Paste(String),
/// 窗口关闭,无附加数据。
Close,
}
impl Event {
/// 把事件渲染成一行人类可读的日志。
fn describe(&self) -> String {
match self {
Event::Click { x, y } => format!("在 ({x}, {y}) 点击"),
Event::KeyPress(c) => format!("按下按键 '{c}'"),
Event::Paste(text) => format!("粘贴了 {} 个字符", text.chars().count()),
Event::Close => "关闭窗口".to_string(),
}
}
}
/// 用 `Option` 表达“可能没有结果”:找不到偶数时返回 `None`。
fn first_even(values: &[i32]) -> Option<i32> {
values.iter().copied().find(|value| value % 2 == 0)
}
/// 把字符串解析成正数:解析失败或非正数都返回 `None`。
///
/// 演示 `and_then`:只有上一步是 `Some` 时才继续,否则短路为 `None`。
fn parse_positive(text: &str) -> Option<i32> {
text.parse::<i32>()
.ok()
.and_then(|n| if n > 0 { Some(n) } else { None })
}
/// 取首个偶数的平方;没有偶数则回退到 0。
///
/// 演示 `map`(在 `Some` 内部变换)与 `unwrap_or`(提供默认值)。
fn first_even_squared_or_zero(values: &[i32]) -> i32 {
first_even(values).map(|n| n * n).unwrap_or(0)
}
fn main() {
println!("== 枚举 + impl 方法 ==");
let shapes = [
Shape::Circle { radius: 1.0 },
Shape::Rectangle {
width: 2.0,
height: 3.0,
},
Shape::Triangle(4.0, 3.0),
];
for shape in &shapes {
println!("{} 面积 = {:.2}", shape.name(), shape.area());
}
println!("\n== 业务枚举 Event ==");
let events = [
Event::Click { x: 10, y: 20 },
Event::KeyPress('A'),
Event::Paste("你好,世界".to_string()),
Event::Close,
];
for event in &events {
println!("{}", event.describe());
}
println!("\n== Option 与组合子 ==");
// match:最完整的处理方式。
match first_even(&[1, 3, 4, 7]) {
Some(value) => println!("首个偶数 = {value}"),
None => println!("没有偶数"),
}
// if let:只关心 Some 分支时的简写。
if let Some(n) = parse_positive("42") {
println!("解析出正数 = {n}");
}
// let ... else:拿不到值就提前返回/退出,主流程保持平铺。
let Some(first) = first_even(&[5, 6, 7]) else {
println!("不应发生:切片里没有偶数");
return;
};
println!("let-else 取到首个偶数 = {first}");
// map / unwrap_or:变换后给默认值。
println!(
"首个偶数的平方(无则0) = {}",
first_even_squared_or_zero(&[1, 3, 5])
);
// unwrap_or_else:默认值需要计算时用闭包,避免无谓开销。
let parsed = parse_positive("oops").unwrap_or_else(|| {
println!("解析失败,使用兜底值");
-1
});
println!("最终值 = {parsed}");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rectangle_area() {
let shape = Shape::Rectangle {
width: 2.0,
height: 3.0,
};
assert_eq!(shape.area(), 6.0);
}
#[test]
fn circle_area_is_positive() {
let shape = Shape::Circle { radius: 1.0 };
assert!((shape.area() - std::f64::consts::PI).abs() < 1e-9);
}
#[test]
fn triangle_area_and_name() {
let shape = Shape::Triangle(4.0, 3.0);
assert_eq!(shape.area(), 6.0);
assert_eq!(shape.name(), "三角形");
}
#[test]
fn finds_first_even() {
assert_eq!(first_even(&[1, 3, 4, 7]), Some(4));
assert_eq!(first_even(&[1, 3, 5]), None);
}
#[test]
fn parse_positive_filters_non_positive() {
assert_eq!(parse_positive("7"), Some(7));
assert_eq!(parse_positive("0"), None);
assert_eq!(parse_positive("-3"), None);
assert_eq!(parse_positive("abc"), None);
}
#[test]
fn squared_or_zero_falls_back() {
assert_eq!(first_even_squared_or_zero(&[1, 3, 4]), 16);
assert_eq!(first_even_squared_or_zero(&[1, 3, 5]), 0);
}
#[test]
fn event_describe_counts_chars() {
let event = Event::Paste("你好".to_string());
assert_eq!(event.describe(), "粘贴了 2 个字符");
}
} examples/11_enums_option/Cargo.toml
[package]
name = "rt_11_enums_option"
version.workspace = true
edition.workspace = true
publish.workspace = true