错误处理:panic 与 Result 的分工 — 第 9 章拆解
错误处理:panic 与 Result 的分工
这一章讲什么: Rust 没有异常。错误的全部处理就是两条路——panic!(程序进入无法恢复的状态, 立即中止)和 Result<T, E>(操作可能失败,把失败当值传回去,由调用方决定怎么办)。 读完你要会:用 match/unwrap/expect 处理 Result;用
?传播错误;以及最重要的判断题—— 什么时候该 panic,什么时候该返回 Result。这一章和所有权并列,是"编译器守门人"哲学的另一半。
1. 顶层全景:一张决策图
主线例子:打开文件 hello.txt 并读出用户名——它会在本章被重写四遍,一遍比一遍短。
错误分两类(Rust 强迫你选边):
┌─ 不可恢复(= bug,如数组越界)──────────→ panic!(中止程序)
└─ 可恢复(如文件不存在)───────────────→ Result<T, E>(错误是个值,可以传、可以判)
处理三档:
match(精细)→ unwrap/expect(偷懒,失败即 panic)→ ?(传播给调用方)
图说:其他语言用一套机制(异常)糊住两类错误;Rust 把它们拆开,
拆开的收益全在"调用方的选择权"上。2. panic!:不可恢复错误的唯一出口
两种触发方式:程序自己干出无法收场的事(如数组越界),或显式调用 panic!("crash and burn")。默认行为:打印失败信息、回栈(unwind)清理、退出。回栈是 Rust 沿调用栈往回走、逐个函数清理数据;嫌清理贵可以改成 panic = 'abort'(在 Cargo.toml 的 [profile] 段),效果是立刻终止、把内存留给操作系统收拾——换更小的二进制1。
panic 信息给出错的文件、行号和消息;设 RUST_BACKTRACE=1 环境变量可以再拿到回溯(backtrace,一路到出错点的函数调用链)。书提醒一件事:报出的位置可能是库代码里 panic! 所在的那一行(比如越界检查在标准库里),不是你犯错的那行——回溯的作用就是往回找到你自己的代码2。
3. Result<T, E>:可恢复错误是"一个值"
Result<T, E> 是标准库枚举:Ok(T) 成功、Err(E) 失败——第 2 章就见过。与 panic 的本质区别:失败不跳流程,而是变成一个可以传递、可以检查的值。File::open("hello.txt") 返回 Result<File, io::Error>;文件不存在时错误长这样:Os { code: 2, kind: NotFound, message: "No such file or directory" }3。
处理的第一档:match。 最朴素的写法是对 Ok/Err 两臂分别处理。想对失败原因区别对待(文件不存在就创建、别的错误才 panic),在内层再 match 一次 error.kind()(io::ErrorKind 枚举)4:
let greeting_file = match File::open("hello.txt") {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Problem creating the file: {e:?}"),
},
_ => panic!("Problem opening the file: {error:?}"),
},
};第二档:unwrap / expect——失败即 panic 的快捷键。 unwrap():Ok 取其值,Err 替你 panic。expect("消息"):同上,但 panic 信息用你给的这句。生产代码里多数 Rust 程序员选 expect 并写清"为什么这里预期必然成功"——将来假设被证伪时,调试信息里带着理由5。
第三档(? 传播)是下一节。书还预告:第 13 章的闭式方法(unwrap_or_else 等)能把上面嵌套的 match 大幅收拾干净6。
4. ?:错误传播的一个字符
场景:函数内部调用可能失败的操作,但函数本身不处理——把错误原样交给调用方,那里信息更多、更有权决定。 这叫传播(propagating)7。
先手工写一遍(读用户名文件,两个可能失败的操作):
fn read_username_from_file() -> Result<String, io::Error> {
let username_file_result = File::open("hello.txt");
let mut username_file = match username_file_result {
Ok(file) => file,
Err(e) => return Err(e), // 失败:整个函数早退,把错误传上去
};
let mut username = String::new();
match username_file.read_to_string(&mut username) {
Ok(_) => Ok(username),
Err(e) => Err(e),
}
}返回类型 Result<String, io::Error> 的含义:成功给调用方一个 String(用户名),失败给一个 io::Error(里面装着两种操作各自的失败详情)。调用方拿到 Err 后 panic、用默认值、换个来源取,都随它8。
? 就是把上面这套 match 压成一个后缀:
fn read_username_from_file() -> Result<String, io::Error> {
let mut username_file = File::open("hello.txt")?;
let mut username = String::new();
username_file.read_to_string(&mut username)?;
Ok(username)
}? 的语义:Ok 则取出里面的值继续;Err 则整个函数立刻返回这个 Err。 注意它和手工 match 有一处不同:? 会走 From trait 的 from 函数,把错误值自动转成当前函数签名里声明的错误类型——函数声明一个统一的 OurError,只要实现 From<io::Error> for OurError,各处 ? 自动完成转换,函数体零额外代码9。
还可以更短:链式 File::open("hello.txt")?.read_to_string(&mut username)?;,或者直接 fs::read_to_string("hello.txt")(标准库把"打开+读全文"打包好了;前面绕路是为了把机制讲全)10。
? 的两条边界
边界一:只能在返回类型兼容的函数里用。 ? 的本质是"可能早退一个 Err",所以函数必须返回 Result(或 Option 等实现了 FromResidual 的类型)。普通 fn main() 返回 (),在里面用 ? 直接编译错——报错信息会建议你改成 fn main() -> Result<(), Box<dyn Error>>11。
边界二:Result 的 ? 和 Option 的 ? 不能混用。 ? 用在 Option 上,要求函数也返回 Option:None 早退,Some 取值继续。例:text.lines().next()?.chars().last() 一行实现"取首行最后一个字符"(空文本、空行都自然落在 None)。要跨界转换得显式:Result::ok、Option::ok_or12。
main 返回 Result 的彩蛋:fn main() -> Result<(), Box<dyn Error>> 合法。Box<dyn Error> 先读作"任何错误"(trait 对象,第 18 章);好处是以后 main 里加了返回别的错误类型的代码,签名也不用改。程序退出码跟着 Result 走:Ok 退 0,Err 退非 0——与 C 的惯例兼容13。
5. 主线例子的落点:什么时候 panic,什么时候 Result
书末的判断准则,值得整段带走:
默认:定义"可能失败"的函数,返回 Result 是好的默认选择。 panic 等于替调用方宣布"此错不可恢复"——这个决定权应该上交;Result 把选择权留给调用方(它若想,也可以对你的 Err 再 panic)14。
panic 合适的场合15:
- 示例、原型、测试:示例里塞满错误处理会喧宾夺主;原型阶段 unwrap/expect 是"以后回来补"的路标;测试失败本身就是 panic,unwrap 恰是正确姿势。
- 你比编译器知道得多:硬编码的 IP 字符串
"127.0.0.1".parse().expect("Hardcoded IP address should be valid")——逻辑上不可能失败,但 parse 的返回类型还是 Result,编译器逼你表态;此时 expect + 写明理由,并把它当成"将来若改从用户输入解析,就回来换实现"的警示桩。 - 坏状态(bad state):假设/契约/不变量被破坏(非法值、矛盾值、缺值),且满足以下任一条:坏状态出乎意料(不是"用户偶尔输错格式"那种预期内的失败);后续代码每一步都依赖"不坏",没法逐步检查;类型系统编不下这个约束。
库代码的细分16:调用方传参不合理,尽量返回错误让它自己定夺;但继续执行可能不安全时,panic 是对的——把调用方的 bug 当场炸出来,比在开发期漏过去强。标准库对越界访问 panic 就是这个理由:访问不属于这个数据结构的内存是安全问题。函数契约(输入须满足的条件)被违反时 panic 也合理——契约违反说明是调用方的 bug,没有可恢复的路;会 panic 的契约要写进 API 文档。
6. 把校验编进类型:Guess
第 5 节的"类型系统编不下这个约束"有解法——造一个只能合法的类型。回看第 2 章的猜数字:用户猜 1–100,但当时的代码没真正拦住越界值(只是输出了"太大/太小")。若"值必须在 1 到 100"对整个程序很要紧、且有多个函数依赖它,每个函数入口都查一遍既烦又可能拖性能。
pub struct Guess {
value: i32,
}
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 || value > 100 {
panic!("Guess value must be between 1 and 100, got {value}.");
}
Guess { value }
}
pub fn value(&self) -> i32 {
self.value
}
}设计要点:字段 value 私有,外部只能走 Guess::new 构造——校验在构造那一刻发生,panic 是"调用方有 bug"的通报;getter 只读开放。于是任何"只接受 1–100"的函数,签名里写 Guess 而非 i32,函数体一次校验都不用做:编译器担保传进来的 Guess 必然合法。无效值连编译都过不了——检查从运行时搬到了编译期17。
7. 作者的判断与证据
- "返回 Result 是好的默认"——价值主张,理由是选择权归属14。
- panic 三条件(意外/依赖/编不进类型)——经验准则,书里明说部分例子要到第 18 章"用类型编码状态"才闭环。
- Guess 例子——机制论证,校验成本从 N 个函数降到 1 个构造函数17。
判断(我们的,不是书里的): 本章的分工可以概括成一句:panic 是"程序员的 bug", Result 是"世界的无常"。 用户输错、文件丢了、网络断了——Result; 不变量被破坏、契约被违反——panic,因为那意味着代码写错了,不是世界错了。 这个二分让"错误处理"在 Rust 里不再是防御性编程,而是接口设计的一部分。 如果错,会错在: 边界是灰的——有的"坏状态"其实是预期失败(如解析外部数据), 书里也确实把"解析器拿到畸形数据"归 Result 而非 panic;判据是"调用方有没有合理的恢复动作", 不是错误本身的严重程度。
8. 边界与局限
?依赖的 FromResidual trait 只在报错信息里露面,机制完全没讲(属进阶)。- 自定义错误类型的完整做法(thiserror 等生态惯例)超出本书;第 20 章只给了 From 的接口概念。
- main 的 Termination trait 提了一句,细节指文档。
9. 可带走的
- panic = 不可恢复:打印、清理(默认 unwind,可改 abort)、退出。
RUST_BACKTRACE=1拿调用链;报错行可能在库里,不是你的代码。- Result 的失败是值:可以 match、可以传、可以存。
- unwrap/expect:失败即 panic;生产代码用 expect 并写明"为什么预期成功"。
?:Ok 取值继续,Err 函数早退;会经 From 自动转换错误类型。?只能用在返回 Result/Option 的函数里;两种?不能混,ok/ok_or 显式转换。fn main() -> Result<(), Box<dyn Error>>让 main 也能用?;退出码跟 Ok/Err 走。- 库代码默认返回 Result;panic 留给 bug、示例、原型、测试,以及"继续跑会不安全"。
- 契约违反要 panic,且写进文档;预期内的失败(解析坏数据、限流)必须 Result。
- 用私有字段 + 校验构造函数(Guess 模式)把运行时检查变成编译期担保。
10. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| unwind 与 abort | Chapter 9: Error Handling | text/16-ch09-chapter-9-error-handling.txt:36(搜「THE」) |
| backtrace 用法 | 同上 | text/16-ch09-chapter-9-error-handling.txt:74(搜「backtrace」) |
| Result 与 File::open | 同上 | text/16-ch09-chapter-9-error-handling.txt:249(搜「Problem opening the file」) |
| 分错误种类处理 | 同上 | text/16-ch09-chapter-9-error-handling.txt:253(搜「Matching on Different Errors」) |
| unwrap / expect | 同上 | text/16-ch09-chapter-9-error-handling.txt:336(搜「shortcut」) · text/16-ch09-chapter-9-error-handling.txt:374(搜「production-quality」) |
| 手工传播错误 | 同上 | text/16-ch09-chapter-9-error-handling.txt:393(搜「read_username_from_file」) |
| ? 运算符与 From | 同上 | text/16-ch09-chapter-9-error-handling.txt:473(搜「almost the same」) · text/16-ch09-chapter-9-error-handling.txt:482(搜「from function」) |
| ? 的可用范围 | 同上 | text/16-ch09-chapter-9-error-handling.txt:552(搜「compatible」) |
| ? 用于 Option | 同上 | text/16-ch09-chapter-9-error-handling.txt:603(搜「last_char_of_first_line」) |
| main 返回 Result | 同上 | text/16-ch09-chapter-9-error-handling.txt:646(搜「Box<dyn Error>」) |
| panic or not 准则 | 同上 | text/16-ch09-chapter-9-error-handling.txt:676(搜「To panic! or Not」) · text/16-ch09-chapter-9-error-handling.txt:735(搜「Guidelines」) |
| Guess 类型 | 同上 | text/16-ch09-chapter-9-error-handling.txt:832(搜「pub struct Guess」) |
Footnotes
-
出处:「Chapter 9: Error Handling」第 36 段(
text/16-ch09-chapter-9-error-handling.txt:36,搜「THE」)。 ↩ -
出处:「Chapter 9: Error Handling」第 72 段(
text/16-ch09-chapter-9-error-handling.txt:72,搜「someone else’s code」)。 ↩ -
出处:「Chapter 9: Error Handling」第 249 段(
text/16-ch09-chapter-9-error-handling.txt:249,搜「Problem opening the file」)。 ↩ -
出处:「Chapter 9: Error Handling」第 253 段(
text/16-ch09-chapter-9-error-handling.txt:253,搜「Matching on Different Errors」)。 ↩ -
出处:「Chapter 9: Error Handling」第 336 段(
text/16-ch09-chapter-9-error-handling.txt:336,搜「shortcut」)与第 374 段(text/16-ch09-chapter-9-error-handling.txt:374,搜「production-quality」)。 ↩ -
出处:「Chapter 9: Error Handling」第 303 段(
text/16-ch09-chapter-9-error-handling.txt:303,搜「Alternatives to Using match」)。 ↩ -
出处:「Chapter 9: Error Handling」第 379 段(
text/16-ch09-chapter-9-error-handling.txt:379,搜「Propagating Errors」)。 ↩ -
出处:「Chapter 9: Error Handling」第 393 段(
text/16-ch09-chapter-9-error-handling.txt:393,搜「read_username_from_file」)。 ↩ -
出处:「Chapter 9: Error Handling」第 473 段(
text/16-ch09-chapter-9-error-handling.txt:473,搜「almost the same」)与第 482 段(text/16-ch09-chapter-9-error-handling.txt:482,搜「from function」)。 ↩ -
出处:「Chapter 9: Error Handling」第 531 段(
text/16-ch09-chapter-9-error-handling.txt:531,搜「fs::read_to_string」)。 ↩ -
出处:「Chapter 9: Error Handling」第 552 段(
text/16-ch09-chapter-9-error-handling.txt:552,搜「compatible」)与第 567 段(text/16-ch09-chapter-9-error-handling.txt:567,搜「can only be used」)。 ↩ -
出处:「Chapter 9: Error Handling」第 603 段(
text/16-ch09-chapter-9-error-handling.txt:603,搜「last_char_of_first_line」)。 ↩ -
出处:「Chapter 9: Error Handling」第 646 段(
text/16-ch09-chapter-9-error-handling.txt:646,搜「Box<dyn Error>」)与第 663 段(text/16-ch09-chapter-9-error-handling.txt:663,搜「with a value of 0」)。 ↩ -
出处:「Chapter 9: Error Handling」第 676 段(
text/16-ch09-chapter-9-error-handling.txt:676,搜「To panic! or Not」)。 ↩ ↩2 -
出处:「Chapter 9: Error Handling」第 735 段(
text/16-ch09-chapter-9-error-handling.txt:735,搜「Guidelines」)。 ↩ -
出处:「Chapter 9: Error Handling」第 748 段(
text/16-ch09-chapter-9-error-handling.txt:748,搜「values that don’t make sense」)与第 762 段(text/16-ch09-chapter-9-error-handling.txt:762,搜「safety reasons」)。 ↩ -
出处:「Chapter 9: Error Handling」第 832 段(
text/16-ch09-chapter-9-error-handling.txt:832,搜「pub struct Guess」)与第 776 段(text/16-ch09-chapter-9-error-handling.txt:776,搜「rather than an」)。 ↩ ↩2