使用 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!

文件名: src/main.rs
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' 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 表示它是 src/main.rs 文件的第二行,第五个字符。

在这种情况下,指示的行是我们代码的一部分,如果我们转到那一行,我们会看到 panic! 宏调用。在其他情况下,panic! 调用可能在我们代码调用的代码中,并且错误消息报告的文件名和行号将是其他人的代码中调用 panic! 宏的位置,而不是最终导致 panic! 调用的我们代码的行。

我们可以使用 panic! 调用来自的函数的 backtrace 来找出导致问题的代码部分。为了理解如何使用 panic! backtrace,让我们看另一个例子,看看当 panic! 调用来自库时会是什么样子,这是因为我们的代码中存在 bug,而不是我们的代码直接调用了宏。示例 9-1 中的代码尝试访问向量中超出有效索引范围的索引。

文件名: src/main.rs
fn main() { let v = vec![1, 2, 3]; v[99]; }
示例 9-1: 尝试访问向量末尾之外的元素,这将导致调用 panic!

在这里,我们尝试访问向量的第 100 个元素(索引为 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' 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 环境变量来获取导致错误的具体原因的 backtrace。backtrace 是一个到达此点所调用的所有函数的列表。Rust 中的 backtrace 与其他语言中的工作方式相同:读取 backtrace 的关键是从顶部开始读取,直到看到你编写的文件。那是问题起源的地方。该位置上方的行是你的代码调用的代码;下方的行是调用你的代码的代码。这些前后的行可能包括核心 Rust 代码、标准库代码或你正在使用的 crate。让我们尝试通过将 RUST_BACKTRACE 环境变量设置为除 0 以外的任何值来获取 backtrace。示例 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/f6e511eec7342f59a25f7c0534f1dbea00d01b14/library/std/src/panicking.rs:662:5 1: core::panicking::panic_fmt at /rustc/f6e511eec7342f59a25f7c0534f1dbea00d01b14/library/core/src/panicking.rs:74:14 2: core::panicking::panic_bounds_check at /rustc/f6e511eec7342f59a25f7c0534f1dbea00d01b14/library/core/src/panicking.rs:276:5 3: <usize as core::slice::index::SliceIndex<[T]>>::index at /rustc/f6e511eec7342f59a25f7c0534f1dbea00d01b14/library/core/src/slice/index.rs:302:10 4: core::slice::index::<impl core::ops::index::Index<I> for [T]>::index at /rustc/f6e511eec7342f59a25f7c0534f1dbea00d01b14/library/core/src/slice/index.rs:16:9 5: <alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index at /rustc/f6e511eec7342f59a25f7c0534f1dbea00d01b14/library/alloc/src/vec/mod.rs:2920:9 6: panic::main at ./src/main.rs:4:6 7: core::ops::function::FnOnce::call_once at /rustc/f6e511eec7342f59a25f7c0534f1dbea00d01b14/library/core/src/ops/function.rs:250:5 note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
示例 9-2: 当环境变量 RUST_BACKTRACE 被设置时,由 panic! 调用生成的 backtrace 输出

输出很多!你看到的具体输出可能因你的操作系统和 Rust 版本而异。为了获得包含此信息的 backtrace,必须启用调试符号。当使用不带 --release 标志的 cargo buildcargo run 时,调试符号默认是启用的,就像我们这里一样。

在示例 9-2 的输出中,backtrace 的第 6 行指向我们项目中导致问题的行:src/main.rs 文件的第 4 行。如果我们不想让我们的程序 panic,我们应该从第一行提到我们编写的文件的位置开始调查。在示例 9-1 中,我们故意编写了会导致 panic 的代码,修复 panic 的方法是不请求超出向量索引范围的元素。当你的代码将来 panic 时,你需要弄清楚代码正在使用什么值执行什么操作导致了 panic,以及代码应该改为做什么。

我们将在本章稍后的 “要 panic! 还是不要 panic! 章节中回到 panic! 以及何时应该和不应该使用 panic! 来处理错误情况。章节。接下来,我们将看看如何使用 Result 从错误中恢复。