Rust panic
panic 是 Rust 在出现无法挽回的错误时立刻停止程序的方式。如果你一直跟着前面的页面学下来,其实已经在不经意间触发过好几次了:数组索引越界,或者对 None 调用 .unwrap(),都会 panic。
手动触发 panic
Section titled “手动触发 panic”fn main() { panic!("出大问题了");}thread 'main' (12345) panicked at src/main.rs:2:5:出大问题了note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace第一行说明 main 线程在 src/main.rs 第 2 行第 5 列 panic 了,第二行是你自己写的消息,最后的 note 提示你设置 RUST_BACKTRACE=1 就能看到调用栈(backtrace)。括号里的数字(这里是 (12345))是线程 ID。每次运行都会不一样,旧版本的 Rust 则不会显示它。
运行时,程序会打印这条消息然后停下来:panic! 后面的代码一行都不会执行。
不直接调用 panic! 也会 panic 的情况
Section titled “不直接调用 panic! 也会 panic 的情况”你已经见过的一些操作,在出问题时会自己 panic。下面每个例子都通过函数参数拿到那个“危险”的值,而不是直接写死一个字面量。因为如果代码里直接写着字面量索引或者字面量 0,编译器往往能在编译期就看出这个操作必定失败,于是干脆拒绝编译,而不是让它运行起来再 panic。
fn get(numbers: &[i32], index: usize) -> i32 { numbers[index] // panic:索引越界}
fn main() { let numbers = [1, 2, 3]; println!("{}", get(&numbers, 10));}fn main() { let value: Option<i32> = None; println!("{}", value.unwrap()); // panic:called `Option::unwrap()` on a `None` value}fn divide(a: i32, b: i32) -> i32 { a / b // panic:attempt to divide by zero(试图除以零)}
fn main() { let a = 10; let b = 0; println!("{}", divide(a, b));}这些都是 Rust 在拒绝让程序带着一个说不通的状态继续运行,而不是悄悄地返回一堆垃圾数据。
panic! 还是 Result
Section titled “panic! 还是 Result”这是你以后会经常面对的选择:这个失败应该让程序崩溃,还是应该给调用者一个处理它的机会?
panic!:用于 bug,以及代码正确时根本不该发生的情况。这时没有合理的恢复办法,立刻停下来(在造成更大破坏之前)是最安全的选择。Result(在上一页讲过):用于调用者可以合理预料、也想要处理的失败,比如用户输入有误、文件不存在、网络请求失败。
// 用户输入有误:可以恢复,用 Resultfn parse_age(input: &str) -> Result<u32, String> { input.parse().map_err(|_| String::from("不是有效的年龄"))}
// 代码正确时不可能被违反的约定:用 panicfn get_first(numbers: &[i32]) -> i32 { if numbers.is_empty() { panic!("调用 get_first 时传入了空 slice"); } numbers[0]}unwrap() 和 expect() 是 panic 的快捷方式
Section titled “unwrap() 和 expect() 是 panic 的快捷方式”你已经在 Option 和 Result 上都见过 .unwrap() 了:它其实就是“把值给我,没有就 panic”的简写。.expect("消息") 做的是同一件事,但可以附上一条更有用的消息:
fn main() { let input = "42"; let number: i32 = input.parse().expect("input 应该是一个有效的数字"); println!("{number}");}如果解析失败,panic 消息里就会带上你写的文字。比起 .unwrap() 给出的通用消息,这让调试轻松多了。
检查你的假设:assert!、assert_eq! 和 unreachable!
Section titled “检查你的假设:assert!、assert_eq! 和 unreachable!”有时候你确信代码运行到某一处时,某件事一定成立。Rust 提供了几个小宏,可以把这种“确信”变成一次检查。检查失败时,它们会带着清楚的消息 panic,于是 bug 会在它出现的地方立刻暴露,而不是到后面才变成奇怪的结果。
assert!(condition, "消息"):条件为false时 panic。assert_eq!(left, right):两个值不相等时 panic,并把两个值都打印出来。unreachable!("消息"):标记永远不该运行到的代码,比如对一个不可能出现的值写的match分支。
fn average(numbers: &[i32]) -> i32 { assert!(!numbers.is_empty(), "average 至少需要一个数字");
let mut total = 0; for n in numbers { total += n; } total / numbers.len() as i32}
fn day_name(day: u8) -> &'static str { match day { 1 => "星期一", 2 => "星期二", 3 => "星期三", 4 => "星期四", 5 => "星期五", 6 => "星期六", 7 => "星期日", _ => unreachable!("day 必须是 1 到 7,实际是 {day}"), }}
fn main() { let avg = average(&[70, 80, 90]); assert_eq!(avg, 80); println!("平均值:{avg}");
println!("第 3 天是{}", day_name(3));}输出:
平均值:80第 3 天是星期三这里每项检查都通过了,所以程序正常运行。现在在 Playground 里把列表改成 [70, 80, 95]。平均值变成了 81,assert_eq! 会让程序停下来,并把两个值并排列出:
thread 'main' (12345) panicked at src/main.rs:26:5:assertion `left == right` failed left: 81 right: 80assertion `left == right` failed 的意思是“断言 left == right 失败”,下面两行分别是左边和右边的值。再试试 average(&[]) 或 day_name(9),你会在 panic 里看到自己写的消息。
这些检查始终会运行,即使是 release 构建也一样,所以要让它们足够轻量。你会在测试那一页再次见到 assert_eq!,它是测试表达“这个应该等于那个”的主要方式。另外还有 todo!(),它会带着“not yet implemented”(尚未实现)的消息 panic。先把函数的框架搭出来、打算以后再补全时,用它当占位的函数体很方便。
1. 用 expect 解析端口号
Section titled “1. 用 expect 解析端口号”你有一个配置值 let port_text = "8080";。用 .parse() 和 .expect(...) 把它转换成 u16,消息要说明出了什么问题。然后打印 正在监听端口 8080。提示:变量需要写上类型 let port: u16 = ...,parse 才知道要生成什么。之后把文本改成 "80a0",看看 panic 里出现你写的消息。
查看答案
fn main() { let port_text = "8080"; let port: u16 = port_text .parse() .expect("配置里的端口应该是 0 到 65535 之间的数字"); println!("正在监听端口 {port}");}输出:
正在监听端口 80802. 分摊账单
Section titled “2. 分摊账单”写一个函数 fn split_bill(total: u32, people: u32) -> u32,返回每个人应付的金额。在零个人之间分摊账单,说明调用方的代码有 bug,所以用 assert! 加一条清楚的消息来抓住它。打印总额 120、4 个人时每人应付的金额。提示:把 assert! 放在函数的第一行,在除法之前。
查看答案
fn split_bill(total: u32, people: u32) -> u32 { assert!(people > 0, "split_bill 至少需要一个人"); total / people}
fn main() { let share = split_bill(120, 4); println!("每人应付 {share}");}输出:
每人应付 30panic!会立刻停止程序,并打印一条错误消息。- 一些标准操作(数组索引越界、对
None调用.unwrap()、除以零)会自动 panic。 panic!用于本不可能发生的 bug 和被违反的约定;调用者能够合理处理的失败,用Result。.expect("消息")和.unwrap()一样,但附带一条消息,让 panic 更容易调试。- 当你认为成立的事情其实不成立时,
assert!、assert_eq!和unreachable!会故意 panic。
小测验
1. 调用 panic! 会发生什么?
2. 按照本页的判断标准,什么时候应该用 panic! 而不是 Result?
3. .unwrap() 和 .expect('消息') 有什么区别?
得分: 0 / 3
- 用 panic! 处理不可恢复的错误 — Rust 程序设计语言:panic 时到底发生了什么、栈展开(unwinding),以及怎么阅读 backtrace。
- 要不要 panic! — Rust 程序设计语言:官方关于在
panic!和Result之间如何选择的指导,附有示例。 - panic! — 通过例子学 Rust:一个简短的 panic 示例,展示程序在栈展开时依然会释放内存。
assert_eq!— 标准库文档(英文):断言宏是怎么工作的,包括自定义消息。panic!— 标准库文档(英文):panic!本身的参考文档,它和unwrap的关系,以及什么时候 panic 才是正确的工具。