讲解
Rust 把失败分成两类,各有对应机制。可恢复错误——文件不存在、网络超时、用户输入非法——用 Result<T, E>,调用方被迫处理,程序继续运行。不可恢复错误——数组越界、断言失败、逻辑上不该发生的状态——用 panic!:当前线程开始栈展开(unwinding),逐层清理资源后终止。这不是失控崩溃,而是有序撤退;release 模式下也可配置成直接 abort(更快更小的二进制,但不再有清理)。
什么时候 panic 是正当的?三种场景:示例与原型代码(unwrap/expect 快速推进);测试(断言失败本就靠 panic 工作);以及「如果这里失败说明程序有 bug」的不变量检查——比如内部索引逻辑本应保证不越界,真越界就该 panic 暴露 bug 而不是吞掉。写给别人用的库,对外接口应当返回 Result,把 panic 的决定权留给调用方。
unwrap 和 expect("原因") 是「Result 转 panic」的显式开关:确定不会失败,或失败了也只能崩,就用它们,expect 带上一句现场说明。还有一个边界工具 std::panic::catch_unwind 可以在线程内捕获 panic(FFI 边界、测试框架会用到),但它不是常规流程控制手段——日常代码请用 Result。
示例
可恢复错误走 Result + ?,致命不变量才 panic:
fn divide(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
return Err(String::from("除数不能为零"));
}
Ok(a / b)
}
fn average(total_text: &str, count_text: &str) -> Result<f64, String> {
let total: i32 = total_text.parse().map_err(|_| String::from("总数不是数字"))?;
let count: i32 = count_text.parse().map_err(|_| String::from("人数不是数字"))?;
let sum = divide(total, count)?; // 除零错误向上传递
Ok(sum as f64)
}
fn main() {
assert_eq!(average("100", "4"), Ok(25.0));
assert!(average("100", "0").is_err());
assert!(average("abc", "4").is_err());
match average("90", "3") {
Ok(avg) => println!("平均值 {avg}"),
Err(e) => println!("计算失败:{e}"),
}
}
catch_unwind 演示 panic 可以被边界捕获(程序继续运行):
use std::panic;
fn risky(flag: bool) -> i32 {
if flag {
panic!("演示:出现了不该出现的状态");
}
42
}
fn main() {
let ok = panic::catch_unwind(|| risky(false));
assert_eq!(ok.unwrap(), 42);
let crashed = panic::catch_unwind(|| risky(true));
assert!(crashed.is_err());
println!("panic 被捕获,程序正常继续");
// expect:确定不失败时的显式开关
let n: i32 = "42".parse().expect("字面量 42 必然能解析");
assert_eq!(n, 42);
}
常见坑
- 库代码里 panic:库的 panic 会杀死调用方的线程;库返回 Result,panic 与否让应用决定。
- unwrap 散布业务代码:每一个 unwrap 都是一颗定时炸弹;原型阶段可以接受,提交前换成 ?、match 或 expect。
- 把 panic 当异常用:panic 不是 try/catch 体系里的异常,不做流程控制;跨语言习惯在这里要清零。
- expect 信息没营养:panic 时那一行文字是排查的第一线索,写「当时的假设」,不写「出错了」。
小结
Result 处理预期内的失败(? 传递、match 处理),panic 处理「这不该发生」;unwrap/expect 是显式的转换开关;catch_unwind 只在边界用。下一章进入进阶特性第一站:泛型。