错误处理
这一课你将能做到
- 区分 panic! 与 Result 的适用边界,杜绝「图省事一路 unwrap」
- 熟练使用
?运算符传播错误,实现自定义错误类型并完成 From 转换 - 用 Option/Result 组合子链式处理「可能失败」的值
为什么要学
Java 用异常处理错误:try-catch 隐藏在调用栈里,方法签名看不出会不会炸,checked exception 还常年被绕过。Rust 把错误写进函数签名——Result<T, E> 类型上声明「我可能失败,失败长这样」,编译器强制调用方表态。
对高性能后端这有两个实际收益:错误路径无异常展开的开销(返回值即错误),以及读代码时一眼看清所有失败分支。? 运算符让错误传播从 Java 的层层 try-catch 变成一个字符。
先建立图像
Java: Rust:
int parse(String s) fn parse(s: &str) -> Result<u32, ParseIntError>
throws NumberFormatException
调用方三选一(编译器强制):
try-catch 掩盖路径 ① ? 直接上抛
② match 拆开处理
③ unwrap/expect 显式声明「这里不可能失败」
两个错误世界的分工:
panic!:不可恢复的 bug(数组越界、断言失败),对标 Java 的 RuntimeException 直接炸进程/线程Result<T, E>:可预期的失败(解析失败、文件不存在、上游超时),对标 checked exception 但更诚实
跟着做
步骤 1:Result 的基本形状
cargo new err-demo
cd err-demo
use std::num::ParseIntError;
fn parse_time_limit(raw: &str) -> Result<u32, ParseIntError> {
raw.trim().parse::<u32>()
}
fn main() {
match parse_time_limit(" 1000 ") {
Ok(ms) => println!("时限 {ms} ms"),
Err(e) => println!("解析失败:{e}"),
}
match parse_time_limit("abc") {
Ok(ms) => println!("时限 {ms} ms"),
Err(e) => println!("解析失败:{e}"),
}
}
Result<u32, ParseIntError>:成功带 u32,失败带错误详情。" 1000 ".trim().parse() 是标准库字符串转数字的惯用链。
步骤 2:? 运算符——错误传播的最短路径
fn read_limit(line: &str) -> Result<u32, String> {
let (_, raw) = line.split_once('=').ok_or("缺少 '=' 分隔符")?;
// ^ 拆不出 '=' 就提前返回 Err(&str 自动转 String)
let limit = raw.trim().parse::<u32>().map_err(|e| e.to_string())?;
// ^ 解析失败自动 return Err
Ok(limit)
}
fn main() {
println!("{:?}", read_limit("time_limit = 2000"));
println!("{:?}", read_limit("garbage-line"));
}
? 做三件事:检查 Result、失败则携带错误提前 return、成功则取出 Ok 里的值。一个字符顶掉 Java 的三层 try-catch。注意 ? 只能在返回 Result 的函数里用。
步骤 3:自定义错误类型——用 enum 归类失败
use std::fmt;
#[derive(Debug)]
enum ConfigError {
MissingField(&'static str),
BadValue { field: &'static str, raw: String },
}
impl fmt::Display for ConfigError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ConfigError::MissingField(name) => write!(f, "缺少字段:{name}"),
ConfigError::BadValue { field, raw } => write!(f, "字段 {field} 的值非法:{raw}"),
}
}
}
impl std::error::Error for ConfigError {} // 接入标准错误体系,使其可与 ? 兼容
fn parse_limit(raw: &str) -> Result<u32, ConfigError> {
if raw.is_empty() {
return Err(ConfigError::MissingField("time_limit"));
}
raw.trim().parse::<u32>()
.map_err(|_| ConfigError::BadValue { field: "time_limit", raw: raw.to_string() })
}
fn main() -> Result<(), ConfigError> {
let limit = parse_limit("1000")?;
println!("时限 {limit} ms");
let bad = parse_limit("");
println!("{bad:?} / {bad}");
Ok(())
}
自定义错误三件套:enum 列出失败种类、实现 Display 给人看、实现 Error trait 接入生态(之后可被 Box<dyn Error> 收编、可被 ? 转换)。社区常直接用 thiserror 宏生成这三件套,思路完全一致(第 10 课)。
步骤 4:跨错误类型的自动转换
use std::num::ParseIntError;
// Box<dyn Error> 能收编所有实现了 Error 的类型;? 自动用 From 转换
fn load_config(text: &str) -> Result<u32, Box<dyn std::error::Error>> {
let first = text.lines().next().ok_or("空配置")?; // &str 自动转 Box<dyn Error>
let (_, value) = first.split_once('=').ok_or("缺少 = 分隔")?;
let limit: u32 = value.trim().parse()?; // ParseIntError 自动转 Box
Ok(limit)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let limit = load_config("time_limit=1500")?;
println!("加载成功:{limit} ms");
Ok(())
}
main 也能返回 Result:Err 时程序以非零码退出并打印错误,CLI 工具的标准姿势。
步骤 5:Option 与 Result 的组合子工具箱
fn main() {
let score: Option<u32> = Some(88);
let missing: Option<u32> = None;
// map:对 Some 里的值变形,None 原样穿透
let doubled = score.map(|s| s * 2);
println!("{doubled:?}");
// unwrap_or / unwrap_or_else / unwrap_or_default:缺省值三兄弟
println!("{}", missing.unwrap_or(0));
println!("{}", missing.unwrap_or_else(|| 60)); // 惰性求值
println!("{:?}", missing.unwrap_or_default());
// ok_or:Option 转 Result;and_then:链式处理可能失败的步骤
let ok: Result<u32, &str> = score.ok_or("缺考");
println!("{ok:?}");
let pass = score.and_then(|s| if s >= 60 { Some("及格") } else { None });
println!("{pass:?}");
}
组合子是「不 panic 地处理缺失」的优雅路径;unwrap() 只留给测试和可证明安全的场景。
讲清楚要点
- 错误在签名里:
Result<T, E>让失败路径成为类型契约的一部分,对标 Java 方法不写 throws 但运行时照样抛的隐性风险。 ?的完整语义:失败提前返回 + 错误类型自动From转换 + 成功解包取值;只在返回 Result(或 Option)的函数中可用。- 错误类型设计原则:业务层用自定义 enum(调用方好 match),工具层/原型用
Box<dyn Error>(省事),二进制入口用 anyhow 风格(只需展示无需处理)。 - panic 的纪律:只用于 bug(不变量被破坏);用户输入、外部 IO 一律 Result。库代码 panic 是接口事故。
- #[must_use] 强制表态:Result 带有 must_use 属性,返回值被忽略时编译器警告——「调用了但没处理错误」在 Rust 里藏不住。
你可能会懵的地方
-
?和 Java 的 throw 哪个开销大??是零成本:失败时就是一次提前 return,无栈展开、无捕获表。Java 异常首次抛出需要填充栈轨迹,热路径上的异常是真实的性能税。 -
Option 和 Result 为什么不合并成一个? Option 表达「没有值」(不是错误),Result 表达「失败了且带原因」。
ok_or/ok()两者互转;语义分开让函数签名更诚实。 -
什么时候该 panic? 测试断言、程序自身不变量被破坏(如配置加载在启动阶段失败)、示例代码。服务运行期的外部输入永远走 Result。
-
为什么
?需要错误类型实现 From??遇到Err(E1)而函数返回Err(E2)时,自动调From::from(E1)转换。这是「底层错误翻译成业务错误」的机制,免掉手写 match 转发。 -
Box<dyn Error> 和自定义 enum 怎么选? CLI 和原型直接
Box<dyn Error>省事;库和核心业务定义 enum,调用方才能精确 match 分支处理,而不是拿字符串猜原因。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
业务代码里 unwrap() 一把梭 | None/Err 直接 panic | ?、unwrap_or、expect("明确原因") |
在返回 () 的函数里用 ? | error[E0277] 不支持 | 函数签名改为 -> Result<T, E> |
| 自定义错误没实现 Error/Display | ? 转换、打印都不工作 | 实现 Display + impl Error for ... |
| 用 panic 处理用户输入 | 输入不可控必炸 | 全部走 Result,错误信息返回给调用方 |
忽略返回的 Result(如 file.write(...);) | warning: unused must_use | let _ = ... 显式丢弃,或 ? 上抛,或 .ok() |
卡点急救
- error[E0277] the
?operator can only be used...:所在函数必须返回 Result/Option。改签名,或在末尾Ok(...)收尾;小工具直接让 main 返回Result<(), Box<dyn Error>>。 - 错误类型转不过去(? 报 From 不匹配):给目标错误实现
impl From<SourceErr> for TargetErr,或先用map_err手动翻译再?。 - 调试时想快速看错误全貌:临时
.unwrap()定位,修复后换回expect("上下文说明");用dbg!(&result)打印整棵 Result 而不触发 panic。
面试常问
-
Rust 的 Result 和 Java 异常体系的对比? Result 把失败写进类型签名,调用方被编译器强制处理,错误路径零展开开销;Java 异常是隐藏控制流,checked exception 常被吞掉,热路径抛异常有栈轨迹成本。Rust 用「值」建模错误,Java 用「跳转」建模错误。
-
?运算符做了什么? 三合一:Err 时提前 return;返回类型不同时经 From 自动转换;Ok 时解包取值。它是 Rust 错误传播链可读性远超 try-catch 嵌套的关键。 -
panic 与 Result 的选型原则? panic 留给「程序 bug / 不变量被破坏」,表现为快速失败;一切依赖外部输入(用户、文件、网络)的失败走 Result。库的公共 API 里 panic 是事故,panic 边界应收敛为 Result。
自测三问
?遇到Err(E1)且函数返回Result<T, E2>时发生了什么?- 为什么自定义错误要同时实现 Display 和 Error?
map、and_then、unwrap_or_else各适合哪类链式处理?
小结一句话
错误是值不是异常:签名即契约、? 即传播、match 即处理——Rust 把 Java 里最容易糊弄过去的错误处理变成了编译器盯着的必修课。
延伸
- The Rust Book ch09:错误处理
- 下一课:Trait 与泛型——Rust 的「接口」与「模板」,静态分发碾压反射的性能来源。