使用 panic! 处理不可恢复错误
有时,代码中会发生无法挽回的问题。遇到这种情况时,Rust 提供了 panic! 宏。实际中触发 panic 有两种方式:执行会导致代码 panic 的操作(例如访问数组末尾之后的位置),或者显式调用 panic! 宏。这两种方式都会使程序发生 panic。默认情况下,panic 会打印失败消息、展开调用栈、清理栈上的数据,然后退出。你还可以通过环境变量让 Rust 在发生 panic 时显示调用栈,从而更容易追踪 panic 的来源。
panic 时展开栈或直接终止
默认情况下,发生 panic 时,程序会开始展开调用栈,也就是说,Rust 会沿着调用栈返回,并清理所经过的每个函数中的数据。不过,回溯并清理需要做很多工作。因此,Rust 允许你选择另一种方式:立即终止,也就是结束程序而不进行清理。
此时,程序正在使用的内存将由操作系统清理。如果你的项目需要尽可能减小最终生成的二进制文件,可以在 Cargo.toml 文件相应的 [profile] 配置段中添加 panic = 'abort',让程序在 panic 时从展开调用栈切换为直接终止。例如,要让程序在 release 模式下遇到 panic 时终止,可以添加:
[profile.release]
panic = 'abort'
我们在一个简单程序中调用 panic! 来试试看:
fn main() {
panic!("crash and burn");
}
运行程序时,你会看到类似下面的输出:
$ cargo run
Compiling panic v0.1.0 (file:///projects/panic)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.25s
Running `target/debug/panic`
thread 'main' (6018279) panicked at src/main.rs:2:5:
crash and burn
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
调用 panic! 会产生最后两行所示的错误消息。第一行显示了 panic 消息,以及源代码中发生 panic 的位置:src/main.rs:2:5 表示 panic 出现在 src/main.rs 文件的第 2 行第 5 个字符处。
在这个例子中,所指向的行属于我们的代码;跳转到该行,就能看到 panic! 宏的调用。在其他情况下,panic! 调用可能出现在我们的代码所调用的代码中,此时错误消息报告的文件名和行号会指向调用 panic! 宏的其他代码,而不是最终导致该调用的我们自己的代码行。
我们可以查看导致 panic! 调用的函数回溯,找出代码中引发问题的部分。为了了解如何使用 panic! 回溯,我们来看另一个例子:由于我们的代码存在缺陷,库中的代码触发了 panic!,而不是我们的代码直接调用宏。示例 9-1 中的代码尝试访问超出有效索引范围的向量元素。
fn main() {
let v = vec![1, 2, 3];
v[99];
}
panic!这里,我们尝试访问向量的第 100 个元素(由于索引从 0 开始,它的索引是 99),但向量中只有三个元素。在这种情况下,Rust 会触发 panic。使用 [] 本应返回一个元素,但如果传入无效索引,Rust 就无法返回正确的元素。
在 C 语言中,尝试读取数据结构末尾之外的内容属于未定义行为。你可能会得到内存中与该元素对应位置上的任意内容,即使那块内存并不属于该数据结构。这称为缓冲区过读;如果攻击者能够操纵索引,读取数据结构之后本不应允许其访问的数据,就可能造成安全漏洞。
为了保护程序免受此类漏洞影响,如果你尝试读取不存在索引处的元素,Rust 会停止执行并拒绝继续运行。我们来试试看:
$ cargo run
Compiling panic v0.1.0 (file:///projects/panic)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.27s
Running `target/debug/panic`
thread 'main' (6017887) panicked at src/main.rs:4:6:
index out of bounds: the len is 3 but the index is 99
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
这个错误指向 main.rs 的第 4 行,也就是我们尝试访问 v 中索引 99 处元素的位置。
note: 这一行告诉我们,可以设置 RUST_BACKTRACE 环境变量,以获取导致错误的完整调用回溯。回溯是到达当前位置之前调用过的所有函数的列表。Rust 中的回溯与其他语言中的回溯类似:阅读回溯的关键是从顶部开始,向下读到自己编写的文件。那就是问题的起源位置。该位置上方的行是你的代码调用的代码;下方的行则是调用了你的代码。前后这些行可能包含 Rust 核心代码、标准库代码或你所使用的 crate。我们将 RUST_BACKTRACE 环境变量设为除 0 以外的任意值,来查看回溯。示例 9-2 展示了类似的输出。
$ RUST_BACKTRACE=1 cargo run
thread 'main' panicked at src/main.rs:4:6:
index out of bounds: the len is 3 but the index is 99
stack backtrace:
0: rust_begin_unwind
at /rustc/4d91de4e48198da2e33413efdcd9cd2cc0c46688/library/std/src/panicking.rs:692:5
1: core::panicking::panic_fmt
at /rustc/4d91de4e48198da2e33413efdcd9cd2cc0c46688/library/core/src/panicking.rs:75:14
2: core::panicking::panic_bounds_check
at /rustc/4d91de4e48198da2e33413efdcd9cd2cc0c46688/library/core/src/panicking.rs:273:5
3: <usize as core::slice::index::SliceIndex<[T]>>::index
at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/core/src/slice/index.rs:274:10
4: core::slice::index::<impl core::ops::index::Index<I> for [T]>::index
at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/core/src/slice/index.rs:16:9
5: <alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index
at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/alloc/src/vec/mod.rs:3361:9
6: panic::main
at ./src/main.rs:4:6
7: core::ops::function::FnOnce::call_once
at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/core/src/ops/function.rs:250:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
RUST_BACKTRACE 后,调用 panic! 所生成的回溯输出内容很多!你看到的具体输出可能会因操作系统和 Rust 版本而异。要获得包含这些信息的回溯,必须启用调试符号。像这里一样,在运行 cargo build 或 cargo run 时不使用 --release 标志,调试符号默认处于启用状态。
在示例 9-2 的输出中,回溯第 6 行指向了项目中导致问题的代码:src/main.rs 的第 4 行。如果我们不希望程序发生 panic,就应该从回溯中第一个提到我们所编写文件的位置开始调查。在示例 9-1 中,我们故意编写了会触发 panic 的代码;修复方法是不要请求访问超出向量索引范围的元素。以后代码发生 panic 时,你需要找出代码使用了什么值执行了什么操作,从而导致 panic,并思考代码应该改为执行什么操作。
在本章后面的“该不该使用 panic!”一节中,我们会继续讨论何时应该以及不应该使用 panic! 处理错误情况。接下来,我们将了解如何使用 Result 从错误中恢复。
We’ll come back to panic! and when we should and should not use panic! to
handle error conditions in the “To panic! or Not to
panic!” section later in this
chapter. Next, we’ll look at how to recover from an error using Result.