枚举(Enum)基础
本教程共 78 篇 · 第 29 篇 · 更新于 2026-08-08 · 约 9 分钟阅读
本节目标:用
enum定义”同一个值只能是几种可能之一”的类型,掌握变体携带数据,并认识标准库Option<T>如何用枚举根除空指针。
结构体表达”同时拥有多个字段”。但还有一类数据天然是”多选一”:一周七天之一、一个消息是”文本”还是”图片”、一个操作成功还是失败。Rust 用**枚举(enum)**表达这种”从若干种里选一种”的类型。
你可能会觉得:这种”多选一”用数字也比划得出来啊,比如用 0 表示周一、1 表示周二……确实能,但数字有个大毛病——它不解释自己。看到代码里一个 3,你根本不知道它代表周三还是代表”红色”。枚举把”可选的范围”直接焊进了类型里:一个 Weekday 类型的值,编译器知道它只能是那几种,绝不可能是别的。这就是枚举比裸数字安全、也比裸数字好读的根本原因。
1-1 最简单的枚举
enum Weekday {
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday,
}
fn main() {
let day = Weekday::Monday;
}
enum Weekday 定义了七个”变体(variant)“。一个 Weekday 类型的值,必然是这七种之一。Monday 前加 Weekday:: 命名空间,避免和其他 enum 的同名变体撞车。
这里有个概念新手容易混:上面那个 Weekday 是”枚举类型”,而 Weekday::Monday 是这个类型里的一个具体”枚举值”。类型是模具,值是用模具压出来的成品。函数参数写 Weekday,你就能把任意一个变体传进去——因为它们都是同一个类型,这正是枚举”把多种可能统一成一种类型”的妙处,后面讲类型统一时还会用上。
Note枚举名
PascalCase,变体名也PascalCase。Weekday::Monday这种::路径,和上一章关联函数一个样——都是”去某个命名空间下取东西”。
1-2 变体可以携带数据
枚举真正的威力:每个变体能自带不同结构的数据。比如一个消息可以是纯文本、可以是带图、可以是系统提示:
enum Message {
Quit, // 无数据
Move { x: i32, y: i32 }, // 具名字段,像结构体
Write(String), // 单个值,像元组
ChangeColor(u8, u8, u8), // 三个值,像元组结构体
}
同一个 Message 类型,不同变体带不同类型的数据:Quit 啥也不带,Move 带坐标,Write 带一段字符串,ChangeColor 带 RGB 三原色。这种”一个类型、多种形状”的表达力,结构体做不到——结构体是所有字段永远都在。
你可以把任何数据塞进变体:数字、字符串、结构体,甚至另一个枚举。标准库里就有现成的例子——IpAddr 枚举用 V4(Ipv4Addr) 和 V6(Ipv6Addr) 两个变体,把两种不同结构的 IP 地址统一成一种类型。这正是枚举”统一类型 + 各自带数据”的教科书级用法。
Warning注意
Move { x: i32, y: i32 }这种”结构体式变体”用的是花括号、字段名;Write(String)这种”元组式变体”用圆括号、靠位置。两种写法别混:具名用{}、匿名用()。
1-3 枚举 vs 结构体:怎么选
- 用结构体:数据”同时具备多个字段”,且这些字段通常都在(如用户有姓名、邮箱、年龄)。
- 用枚举:数据”在几种互斥的可能里选一种”(如支付方式:微信 / 支付宝 / 银行卡,一次只用一种)。
一个值”是 A 就不是 B”的语义,用枚举最贴切。而且枚举变体还能各自带不同结构的数据,比”结构体里全用 Option 占位”清晰得多。
有人会问:上面那个 Message,我用四个结构体(QuitMessage、MoveMessage……)不也能表达?能,但立刻遇到一个麻烦——这四个结构体是四种不同的类型,你没法写一个”接收任意消息”的函数,除非写四份重载。而枚举让它们统统是 Message 一种类型,一个函数就能收下所有变体。内聚、统一,这就是枚举相比”一堆结构体”的压倒性优势。
1-4 给枚举也加方法
枚举同样能用 impl 块定义方法,和结构体一模一样:
impl Message {
fn call(&self) {
// 根据是哪种变体做不同处理(具体分支留到 match 章节讲)
}
}
枚举不只是数据,也能有行为。配合下一章的 match,方法内部能针对每个变体写不同逻辑。
1-5 标准库大明星:Option
Rust 里有个无处不在的枚举,必须提前认识——Option<T>,用来表达”可能有值、也可能没有”:
enum Option<T> {
Some(T), // 有值,T 是值的类型
None, // 没有值
}
别的语言用一个特殊值 null / nil 表示”没有”,结果到处是”忘判空就崩溃”的空指针异常。Rust 不用 null,而是要求你显式写出”可能没有”:值要么 Some(具体内容),要么 None。
你不用自己定义这个 enum——它已在标准库预导入,直接用:
let some_number = Some(5);
let no_number: Option<i32> = None;
Some(5) 表示”有个 i32,值是 5”;None 表示”啥也没有”,但类型得标清楚是 Option<i32>(因为 None 本身不带类型信息)。
Option 的发明者背景挺有意思:当年提出 null 的图灵奖得主 Tony Hoare 后来公开道歉,说这是他”十亿美元的错误”——因为空引用在之后几十年里引发了无数崩溃和漏洞。Rust 吸取了这个教训,干脆不用 null,改用 Option 把”可能没有”这件事明写在类型里。
Note
Option的精妙:你想用里面的值,编译器强制你先处理”有没有”的情况。这从语言层面消灭了一大类空指针 bug。这是 Rust 安全哲学在”可能缺失”场景的体现。
1-6 Option 逼你面对”无”
因为 Option<T> 不是 T,你不能直接把它当普通值用。必须先”拆”出来:
let x: Option<i32> = Some(5);
// let y = x + 1; // 错!Option<i32> 不是 i32,不能直接加减
要拿到里面的 5,得用 match(下一章)或 if let(下下章)显式处理 Some 和 None 两种情况。麻烦一点,但换来”绝不意外踩空”。
这种”必须先处理才让用”的别扭,其实是 Rust 故意设计的。它逼你在编译期就把”值可能不存在”这件事想清楚:只要一个变量是 Option<T>,编译器就盯着你,不让它混进只接受 T 的运算里。换句话说,不是 Option 的值,你就能拍胸脯保证它一定有效——空值被圈在 Option 里,出不了圈。
Tip初学觉得 Option 啰嗦,但请相信:它把”空值”从”隐藏的地雷”变成”写在类型里的明示”。每次看到
Option<T>,你就知道”这里可能没值,必须先处理”。这是 Rust 帮你写出健壮代码的关键设计。
1-7 枚举配 match 的预热
枚举配 match 才能真正发挥威力。还是那个 Message 枚举,用 match 按变体分流:
fn handle(msg: Message) {
match msg {
Message::Quit => println!("退出"),
Message::Move { x, y } => println!("移到 ({x}, {y})"),
Message::Write(text) => println!("写:{text}"),
Message::ChangeColor(r, g, b) => println!("颜色 {r},{g},{b}"),
}
}
每个变体对应一个分支,结构体式变体 {} 用字段名解构、元组式变体 () 按位置解构。因为 Message 只有四个变体,列全四个就满足”穷尽”,不用 _ 兜底。这种”按变体分流 + 顺手取数据”正是枚举的杀手锏。
Tip枚举变体如果带数据,解构时变量名随便起(
x, y或a, b都行),它们只在对应分支内有效。下一章会完整讲match的模式语法,包括怎么匹配带数据的变体、怎么用_兜底。这里先建立”枚举 + match = 强大的多分支处理”的印象。
1-8 Option 的常用方法预览
Option 除了用 match 拆,标准库还给它准备了一堆便捷方法,先认识两个最常用的。unwrap() 直接取出里面的值,但如果是 None 就 panic——只适合你”确定不会是 None”的场合:
let x = Some(5);
let v = x.unwrap(); // 5
is_some() / is_none() 则安全地判断”有没有值”,返回 bool,常用于条件判断,不取出值。还有 unwrap_or(默认值):有值取值、没值用你给的默认,比 unwrap 安全。
Warning新手最爱写
unwrap()图省事,但它遇到None会让整个程序崩溃。写练习、确定有值时用用无妨;写正式逻辑时,优先用match/if let显式处理None,或用unwrap_or给兜底,别让程序因一个空值挂掉。
1-9 一个常见坑:枚举的大小从哪来
你可能会好奇:一个枚举值占用多少内存?答案是——它等于”最大那个变体”的大小,外加一点区分变体的标签。比如 Message 里 ChangeColor(u8,u8,u8) 占了 3 字节,那整个 Message 至少就得留 3 字节,因为 Move { x: i32, y: i32 } 要 8 字节,所以整体按最大的来。编译器会按”容纳所有变体”来安排内存。
这解释了为什么枚举变体可以各自带完全不同的数据:Rust 在底层统一留足空间,再用一个隐藏的”标签”记住当前到底是哪种变体。你写代码时不用管这些,但理解”枚举值大小由最大变体决定”,能帮你避开”为了省空间硬塞数据进枚举”的误区——变体差异太大时,反而更适合拆成独立结构体。
1-10 枚举配合 if let 的轻量处理
枚举最常见是和 match 一起用(下一章专讲),但日常也有很多”只关心其中一种变体”的轻量场景,用 if let 更省事。比如你拿到一个 Option<i32>,只想在”有值”时打印,用 if let Some(v) = opt { println!("{v}"); } 就够了,不必为 None 也写一个分支。再比如 Result,只关心成功、失败时先不管,同样用 if let Ok(val) = res { ... }。这种”单一模式”的简写在真实代码里出现频率极高。
Note枚举的变体只要带了数据(如
Some(i32)),解出来的数据就”属于这一次匹配”——在if let Some(v) = opt的块里,v是临时绑定,出了块就不可见。这和match里绑定变量的作用域规则一致,不必担心”值被挪走后还能不能再用”:借用检查器会替你盯着。
值得一提的是,Option 不是唯一的标准库枚举。你后面会大量遇到 Result<T, E>(成功存 Ok(值)、失败存 Err(错误)),它和 Option 一样是”用枚举表达可能性”的典范。掌握 enum 的基本写法后,理解这些标准类型只是”换了个变体名字”的事。
1-11 小结
enum 定义”多选一”的类型,变体可带数据(元组式 () 或结构体式 {});与结构体互补:结构体”全都有”,枚举”选一种”。标准库 Option<T>(Some/None)用枚举根除 null。枚举真正发挥威力要配合模式匹配——下一章 match 能按变体分别写逻辑,是 enum 的最佳搭档。