跳转到内容

Rust panic

panic 是 Rust 在出现无法挽回的错误时立刻停止程序的方式。如果你一直跟着前面的页面学下来,其实已经在不经意间触发过好几次了:数组索引越界,或者对 None 调用 .unwrap(),都会 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!:用于 bug,以及代码正确时根本不该发生的情况。这时没有合理的恢复办法,立刻停下来(在造成更大破坏之前)是最安全的选择。
  • Result(在上一页讲过):用于调用者可以合理预料、也想要处理的失败,比如用户输入有误、文件不存在、网络请求失败。
// 用户输入有误:可以恢复,用 Result
fn parse_age(input: &str) -> Result<u32, String> {
input.parse().map_err(|_| String::from("不是有效的年龄"))
}
// 代码正确时不可能被违反的约定:用 panic
fn 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: 80

assertion `left == right` failed 的意思是“断言 left == right 失败”,下面两行分别是左边和右边的值。再试试 average(&[]) 或 day_name(9),你会在 panic 里看到自己写的消息。

这些检查始终会运行,即使是 release 构建也一样,所以要让它们足够轻量。你会在测试那一页再次见到 assert_eq!,它是测试表达“这个应该等于那个”的主要方式。另外还有 todo!(),它会带着“not yet implemented”(尚未实现)的消息 panic。先把函数的框架搭出来、打算以后再补全时,用它当占位的函数体很方便。

你有一个配置值 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}");
}

输出:

正在监听端口 8080
▶ 动手试一试

写一个函数 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}");
}

输出:

每人应付 30
▶ 动手试一试
  • panic! 会立刻停止程序,并打印一条错误消息。
  • 一些标准操作(数组索引越界、对 None 调用 .unwrap()、除以零)会自动 panic。
  • panic! 用于本不可能发生的 bug 和被违反的约定;调用者能够合理处理的失败,用 Result。
  • .expect("消息") 和 .unwrap() 一样,但附带一条消息,让 panic 更容易调试。
  • 当你认为成立的事情其实不成立时,assert!、assert_eq! 和 unreachable! 会故意 panic。

小测验

1. 调用 panic! 会发生什么?

2. 按照本页的判断标准,什么时候应该用 panic! 而不是 Result?

3. .unwrap() 和 .expect('消息') 有什么区别?

得分: 0 / 3