使用 panic! 处理不可恢复的错误

有时,您的代码中会发生糟糕的事情,并且您对此无能为力。在这些情况下,Rust 提供了 panic! 宏。在实践中,有两种方法会导致 panic:采取导致代码 panic 的操作(例如访问数组末尾以外的元素)或显式调用 panic! 宏。在这两种情况下,我们都会在程序中导致 panic。默认情况下,这些 panic 将打印一条失败消息,展开堆栈,清理堆栈并退出。通过环境变量,您还可以让 Rust 在发生 panic 时显示调用堆栈,以便更容易地跟踪 panic 的来源。

展开堆栈或中止以响应 panic

默认情况下,当发生 panic 时,程序开始*展开*,这意味着 Rust 会返回堆栈并清理它遇到的每个函数的数据。但是,这种返回和清理工作量很大。因此,Rust 允许您选择立即*中止*的替代方案,这将在不清理的情况下结束程序。

程序正在使用的内存将需要由操作系统清理。如果在您的项目中,您需要使生成的二进制文件尽可能小,则可以通过在 Cargo.toml 文件中相应的 [profile] 部分添加 panic = 'abort' 来切换到在 panic 时从展开到中止。例如,如果您想在发布模式下中止 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 [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! 调用来自的函数的回溯来找出导致问题的代码部分。我们将在接下来更详细地讨论回溯。

使用 panic! 回溯

让我们看另一个例子,看看当 panic! 调用来自库而不是来自我们直接调用宏的代码时,由于代码中的错误而导致的情况。清单 9-1 中的一些代码尝试访问超出有效索引范围的向量中的索引。

文件名:src/main.rs

fn main() {
    let v = vec![1, 2, 3];

    v[99];
}

清单 9-1:尝试访问向量末尾以外的元素,这将导致调用 panic!

在这里,我们试图访问向量的第 100 个元素(索引为 99,因为索引从零开始),但向量只有 3 个元素。在这种情况下,Rust 将会 panic。使用 [] 应该返回一个元素,但是如果您传递了无效的索引,Rust 就无法在这里返回正确的元素。

在 C 语言中,尝试读取数据结构末尾以外的内容是未定义的行为。您可能会获得与数据结构中该元素相对应的内存位置处的内容,即使该内存不属于该结构。这称为*缓冲区溢出*,如果攻击者能够以读取超出其允许范围的数据结构之后存储的数据的方式来操纵索引,则可能导致安全漏洞。

为了保护您的程序免受此类漏洞的攻击,如果您尝试读取不存在的索引处的元素,Rust 将停止执行并拒绝继续。让我们尝试一下,看看

$ cargo run
   Compiling panic v0.1.0 (file:///projects/panic)
    Finished dev [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 行,我们试图在该行访问索引 99。下一行注释告诉我们,我们可以设置 RUST_BACKTRACE 环境变量来获取导致错误发生的准确回溯。*回溯*是调用到此为止的所有函数的列表。Rust 中的回溯与其他语言中的工作方式相同:读取回溯的关键是从顶部开始读取,直到看到您编写的文件。那就是问题起源的地方。该位置上方的行是您的代码调用的代码;下面的行是调用您的代码的代码。这些前后行可能包括核心 Rust 代码、标准库代码或您正在使用的 crates。让我们尝试通过将 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/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/std/src/panicking.rs:645:5
   1: core::panicking::panic_fmt
             at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/core/src/panicking.rs:72:14
   2: core::panicking::panic_bounds_check
             at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/core/src/panicking.rs:208:5
   3: <usize as core::slice::index::SliceIndex<[T]>>::index
             at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/core/src/slice/index.rs:255:10
   4: core::slice::index::<impl core::ops::index::Index<I> for [T]>::index
             at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/core/src/slice/index.rs:18:9
   5: <alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index
             at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/library/alloc/src/vec/mod.rs:2770:9
   6: panic::main
             at ./src/main.rs:4:6
   7: core::ops::function::FnOnce::call_once
             at /rustc/07dca489ac2d933c78d3c5158e3f43beefeb02ce/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! 生成的回溯

输出信息真多!您看到的准确输出可能会因您的操作系统和 Rust 版本而异。为了获取包含此信息的回溯,必须启用调试符号。默认情况下,在使用 cargo buildcargo run 而不使用 --release 标志时(如我们在此处所做的那样),将启用调试符号。

在清单 9-2 的输出中,回溯的第 6 行指向我们项目中导致问题的行:src/main.rs 的第 4 行。如果我们不希望程序发生 panicking,我们应该从第一行提到的我们编写的文件所在的位置开始调查。在清单 9-1 中,我们故意编写了会导致 panicking 的代码,解决 panicking 的方法是不请求超出向量索引范围的元素。当您的代码在将来发生 panicking 时,您需要弄清楚代码正在采取什么操作、使用什么值导致 panicking 以及代码应该做什么。

我们将在本章后面的“何时应该和不应该使用 panic! 处理错误条件”部分中回到 panic! 以及何时应该和不应该使用 panic! 来处理错误条件。接下来,我们将研究如何使用 Result 从错误中恢复。