结构体与枚举
这一课你将能做到
- 用 struct 建模数据(含方法与关联函数),用 enum 建模「多态状态」
- 熟练使用 match 做穷尽式模式匹配,含解构与绑定
- 用 Option 处理「可能没有」的值,掌握 if let 与 let else
为什么要学
Rust 没有 class 和继承,数据建模靠两个原语:struct 管数据形状,enum 管「可能是这几种情况之一」。组合起来能表达 Java 里要靠继承树或 sealed interface 才能表达的东西,而且 match 的穷尽性检查保证新增状态时所有处理点都被编译器揪出来。
判题系统的核心状态机——AC/WA/TLE/RE——就是 enum 的教科书场景。Java 21 的 record + sealed + switch 模式匹配正在向 Rust 靠拢,学完这课你对两种语言的理解都会加深。
先建立图像
// struct:值的形状(Java 的 record/POJO)
struct Submission {
id: u64,
language: String,
}
// enum:可能的状态集合,每个变体可以携带不同的数据(Java enum + sealed interface 合体)
enum Verdict {
Accepted { time_ms: u32 }, // 结构体风格变体
WrongAnswer(u32, String), // 元组风格:样例号 + 差异信息
CompileError(String), // 单值
TimeLimit, // 无数据
}
match 时编译器强制四种变体全部处理,漏一种就编译失败——这是 Java 的 default 分支给不了的安全性。
跟着做
步骤 1:定义结构体、实现方法
cargo new type-demo
cd type-demo
#[derive(Debug, Clone)] // derive 自动实现调试打印与克隆,第 10 课展开
struct Submission {
id: u64,
language: String,
time_used_ms: u32,
memory_kb: u32,
}
impl Submission {
// 关联函数:没有 self,对标 Java 静态工厂方法。惯例命名 new
fn new(id: u64, language: &str) -> Self {
Self { id, language: language.to_string(), time_used_ms: 0, memory_kb: 0 }
}
// &self:只读方法
fn is_fast(&self) -> bool {
self.time_used_ms < 1000
}
// &mut self:可修改自身
fn record(&mut self, time_ms: u32, mem_kb: u32) {
self.time_used_ms = time_ms;
self.memory_kb = mem_kb;
}
}
fn main() {
let mut sub = Submission::new(1, "cpp");
sub.record(128, 4096);
println!("{sub:?}"); // {:?} 走 Debug derive
println!("达标:{}", sub.is_fast());
}
三种 self:self 拿走所有权(少用)、&self 只读借用(默认选择)、&mut self 可变借用。
步骤 2:枚举携带数据
#[derive(Debug, Clone, PartialEq)]
enum Verdict {
Accepted { time_ms: u32 },
WrongAnswer(u32, String),
CompileError(String),
TimeLimit,
}
fn main() {
let results = vec![
Verdict::Accepted { time_ms: 128 },
Verdict::WrongAnswer(3, "expected 42, got 41".into()),
Verdict::CompileError("undefined reference".into()),
Verdict::TimeLimit,
];
for v in &results {
println!("{v:?}");
}
}
注意每个变体的数据形状都不同——Java 的 enum 只能挂固定字段,Rust 的 enum 是「和类型」(tagged union),表达力对标 sealed interface 的整个继承树。
步骤 3:match 模式匹配
fn describe(v: &Verdict) -> String {
match v {
Verdict::Accepted { time_ms } => format!("AC,耗时 {time_ms} ms"),
Verdict::WrongAnswer(case_id, diff) => format!("WA,样例 #{case_id}:{diff}"),
Verdict::CompileError(msg) => format!("CE:{msg}"),
Verdict::TimeLimit => "TLE".to_string(),
}
}
四个要点:解构直接把变体内部数据拆成局部变量;不需要类型转换与 instanceof;少一个分支编译报错(穷尽性);_ 通配兜底(但核心业务枚举建议显式列全,让新增变体强制暴露)。
步骤 4:Option——用类型表达「可能没有」
fn find(store: &[Submission], id: u64) -> Option<&Submission> {
store.iter().find(|s| s.id == id)
}
fn main() {
// 沿用步骤 1 的 Submission,这里补两行构造测试数据
let mut store = vec![Submission::new(1, "cpp"), Submission::new(2, "java")];
store[0].record(88, 2048);
// if let:只关心命中一种情况
if let Some(sub) = find(&store, 1) {
println!("找到 {},耗时 {} ms", sub.id, sub.time_used_ms);
}
// let else:守卫主流程(Rust 1.65+)
let Some(target) = find(&store, 42) else {
eprintln!("提交 42 不存在");
return;
};
println!("{target:?}");
}
Option<T> 是 enum:Some(T) 或 None。编译器强制你处理 None——Java 的 NullPointerException 在 Rust 里是编译错误。
步骤 5:把两者拼成完整状态机
fn judge(code: &str) -> Verdict {
if code.contains("fn main") {
if code.contains("BUG") { Verdict::WrongAnswer(1, "output mismatch".into()) }
else { Verdict::Accepted { time_ms: 96 } }
} else {
Verdict::CompileError("no entry point".into())
}
}
fn main() {
for code in ["fn main() {}", "fn main() { BUG }", "print(1)"] {
println!("{:?} => {}", code, describe(&judge(code)));
}
}
讲清楚要点
- struct 是积木,enum 是分叉:数据用 struct,状态机/多分支结果用 enum;Rust 的多态首选 enum + match,而非 trait 继承。
- match 是表达式:分支值直接作为结果,所有分支必须同类型;穷尽性检查让「新增枚举变体后漏改的 if-else」这类事故在编译期暴露。
- 关联函数 vs 方法:没有 self 的是关联函数(对标静态方法,惯用
new);按&self/&mut self/self决定对实例的访问级别。 - Option 消灭 null:可能缺失的值类型上就是
Option<T>,配合?(第 6 课)、if let、unwrap_or形成完整工具链。 - derive 省样板:
#[derive(Debug, Clone, PartialEq)]一行顶 Java 手写的 toString/equals/clone 骨架;自定义行为用 impl(第 7、10 课展开)。
你可能会懵的地方
-
没有继承,怎么复用代码? 组合 + trait(第 7 课)。数据复用直接把 struct 作为字段嵌进去;行为复用定义 trait 再实现。继承的「脆弱基类问题」在 Rust 里从根上不存在。
-
enum 变体的数据怎么「共享部分字段」? 变体之间不共享字段,这是刻意的:把公共字段提到外层 struct(如
struct Result { submission_id: u64, verdict: Verdict }),比 Java 继承基类更扁平。 -
match 和 Java switch 的区别? match 可解构任意嵌套的模式、可加绑定、可带守卫
if条件,且强制穷尽;Java 21 的模式匹配 switch 是向它学习的产物,但默认非穷尽。 -
Option<T> 性能上有开销吗? 没有。编译器对小类型做了优化(niche optimization),
Option<&T>与裸指针同大小,判空就是判指针,零成本。 -
什么时候用 unwrap 是合理的? 测试代码、原型阶段、以及你能证明 None 不可能出现的场景(并留注释)。生产代码用
?、unwrap_or、expect("原因")把缺失路径说清楚。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| struct 字段没全部初始化 | error[E0063] 缺字段 | 补齐字段,或用 ..Default::default()(需 derive Default) |
| match 漏掉某个变体 | error[E0004] 不穷尽 | 补分支;确属不关心的用 _ 显式兜底 |
if v == Some(x) 直接和裸值比较 | 类型不同无法比较 | if let Some(x) = v 或 matches!(v, Some(_)) |
| 方法签名漏写 self | error: 不是合法方法 | 方法第一个参数必须是 self/&self/&mut self |
把 Option<T> 当 T 用(如直接算术运算) | error[E0369] | 先解包:? / if let / unwrap_or |
卡点急救
- error[E0004] non-exhaustive patterns:缺分支。报错信息会精确列出漏掉的变体名;补上或写
_ =>(但优先显式列全)。 - error[E0308] 分支类型不一致:match 是表达式,所有分支必须同类型;常见坑是某分支漏了
.to_string()返回了&str。 - error[E0596] cannot borrow as mutable:实例不是 mut 声明却调用了
&mut self方法,把绑定改成let mut。
面试常问
-
Rust 的 enum 和 Java 的 enum 有什么本质区别? Rust enum 是代数数据类型(sum type),每个变体携带任意异构数据,等价于 sealed interface + 多个 record;Java enum 变体共享固定字段。表达力上 Rust enum 更强,match 穷尽性检查把它变成类型安全的状态机。
-
Option<T> 为什么比 null 好? null 混进了任何引用类型,编译器无法区分「可能为空」和「绝不为空」;Option 把可能性写进类型,强制处理 None 分支,NPE 这类运行时崩溃被编译期拦截,且零运行时开销。
-
match 的穷尽性在工程上有什么价值? 状态机重构时(新增 RE 变体、合并 WA 分支),所有未更新的处理点全部编译报错,重构安全性由编译器保证,不依赖 grep 和人肉 review。
自测三问
&self、&mut self、self三种方法签名分别适用什么场景?- 写一个 match 分支,同时解构
Verdict::Accepted { time_ms }并只对 time_ms > 1000 输出警告(提示:match 守卫)。 if let、let else、unwrap_or各适合处理 Option 的哪种情况?
小结一句话
struct 定形状、enum 定状态、match 强制穷尽、Option 驱逐 null——Rust 用四个原语完成了 Java 需要 class、enum、sealed、Optional 加继承体系才能拼出的建模能力。
延伸
- The Rust Book ch05+ch06:结构体与枚举
- 下一课:错误处理——panic 与 Result 的分工,
?运算符让错误传播一行搞定。