异常的问题:一条看不见的密道
先想清楚 Rust 在反对什么。Java、Python 的异常机制(exception)是这样的:函数深处出了错,扔出一个异常,它沿着调用链一路向上飞,撞见第一个接住它的 try/catch 为止。问题是——看函数签名,你看不出它会扔什么。一段岁月静好的代码,可能从任何一行蹦出一个异常跳到十万八千里外。错误处理走的是一条看不见的控制流密道。
Rust 的答案是把密道变成明路:错误不是飞出来的,是退回来的——它是一个普通的返回值。类型就是上一章见过的枚举,叫 Result:
enum Result<T, E> {
Ok(T), // 成功,装着结果
Err(E), // 失败,装着错误原因
}
读文件的函数签名是 fn read(...) -> Result<String, io::Error>——「可能失败」写在签名里,白纸黑字。想拿到里面的 String,就必须处理 Err 分支,和 Option 一个道理:失败不是暗坑,是明牌。
问号运算符:一个字符的错误流水线
凡事都手写 match 太啰嗦。真实代码长这样:
use std::fs;
fn read_config() -> Result<String, std::io::Error> {
let text = fs::read_to_string("config.toml")?;
let trimmed = text.trim().to_string();
Ok(trimmed)
}
那个 ? 是 Rust 里最值钱的标点。它跟在 Result 后面,意思是:如果是 Ok,把里面的值拆出来继续;如果是 Err,立刻带着这个错误从当前函数退出去,交还给调用者。一行顶一个 match。而且因为函数的返回类型本身就是 Result,「把错误退出去」和「正常返回」走的是同一条类型通道,编译器全程盯着,类型对不上就报错。
对比异常机制,? 的妙处是:每一个可能的失败点在代码里都看得见——哪个调用会失败、失败后错误往哪走,顺着问号一路读上去就行,没有密道。这也是 Rust 社区那句口号的含义:errors are values(错误是值)。值就要走值的规矩:显式传递、类型检查、不许隐身。
类比:异常像火警——警铃一响全楼的人都跑,你事后才知道是三楼微波炉着了;Result + ? 像快递签收链——每个环节要么签收要么写明「破损退回」,谁经手、在哪退的,面单上一清二楚。
那 panic 算什么?
Rust 也有「炸了」的时候,叫 panic——程序中止当前线程并展开清理(或按配置直接退出)。数组越界访问就会 panic,第三章的整数溢出(调试版)也会 panic。它和 Result 的分工是:
- Result:可预期的失败——文件不存在、网络超时、用户输入格式错。这类失败是业务的一部分,该被处理、被恢复。
- panic:程序自身的 bug——「按设计这里绝不可能发生,发生了说明代码写错了」。此时最诚实的做法是立刻停下,而不是带病运行。
标准库里两个和 panic 直接相关的常用方法:.unwrap()——「是 Ok 就给我值,是 Err 就原地 panic」;.expect("理由")——同款,但 panic 时带上你写的说明。它们适合两种场合:写原型、写测试图快;以及你能论证这里绝不可能失败(比如解析一个你刚亲手拼好的字符串)。库代码里乱用 unwrap 是坏习惯——等于替你的用户决定「这种失败不配被处理」。
一句话分场景:「这事可能会失败」→ 返回 Result;「这事理论上不会失败,失败了是我代码的 bug」→ panic 也行;「我懒得处理,先跑起来再说」→ unwrap,但记得这是欠下的债。
手感小结
Rust 日常代码的错误处理节奏是这样的:底层函数如实返回 Result;中间层用 ? 把处理不了的错误往上递;到了最顶层(main、请求处理器)再 match 一把,决定是记日志、给用户报错还是兜底重试。错误从产生到最终处理,整条链路类型可查、路径可见。配上上一章的 Option——「没有」用 Option,「失败」用 Result——你已经掌握了 Rust 程序里最常打的两种明牌。下一章看容器:Vec、HashMap 和那个让循环写法脱胎换骨的迭代器。