无法恢复的错误与panic!

有时在代码中会发生一些无法控制的坏事。在这种情况下,Rust 有 panic! 宏。实际上有两种方式会导致程序崩溃:通过执行导致代码崩溃的操作(例如访问数组末尾之外的元素)或显式调用 panic! 宏。在这两种情况下,我们都会导致程序崩溃。默认情况下,这些崩溃会打印失败消息,展开,清理堆栈并退出。通过环境变量,您还可以让 Rust 在崩溃发生时显示调用堆栈,以便更容易追踪崩溃的来源。

在发生恐慌时展开堆栈或中止

默认情况下,当发生 panic 时,程序开始 unwinding,这意味着 Rust 会回溯调用栈并清理每个遇到的函数中的数据。然而,回溯和清理是一项繁重的工作。因此,Rust 允许你选择立即 aborting,这会在不进行清理的情况下结束程序。

程序使用的内存将需要由操作系统进行清理。如果你的项目需要使生成的二进制文件尽可能小,可以通过在 Cargo.toml 文件的适当 [profile] 部分中添加 panic = 'abort' 来将 panic 时的行为从展开切换为中止。例如,如果你想在发布模式下在 panic 时中止,添加以下内容:

[profile.release]
panic = 'abort'

让我们在简单的程序中尝试调用panic!

Filename: 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! 调用来源的函数回溯来找出导致问题的代码部分。为了了解如何使用 panic! 回溯,让我们看看另一个例子,看看当 panic! 调用是因为我们代码中的错误而来自库,而不是直接从我们的代码调用宏时的情况。列表 9-1 有一些尝试访问向量中超出有效索引范围的索引的代码。

Filename: src/main.rs
fn main() {
    let v = vec![1, 2, 3];

    v[99];
}
Listing 9-1: Attempting to access an element beyond the end of a vector, which will cause a call to panic!

在这里,我们试图访问向量的第100个元素(索引为99,因为索引从零开始),但向量只有三个元素。在这种情况下,Rust 会引发恐慌。使用 [] 应该返回一个元素,但如果传递了一个无效的索引,这里没有一个元素是 Rust 可以返回的,而且返回的元素也不会是正确的。

在 C 语言中,尝试读取数据结构末尾之外的内容是未定义的行为。你可能会得到位于内存中对应于该数据结构中该元素位置的数据,即使该内存不属于该结构。这称为 缓冲区越界读取,如果攻击者能够以某种方式操纵索引以读取数据结构之后存储的不应被访问的数据,这可能会导致安全漏洞。

为了保护您的程序免受此类漏洞的影响,如果您尝试读取一个不存在的索引的元素,Rust 将停止执行并拒绝继续。让我们试一试:let a = vec![1, 2, 3]; a[10]; 运行此代码将导致程序崩溃,因为索引 10 超出了向量的范围。

$ 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 环境变量来获取导致错误的详细回溯。一个 回溯 是到达此点时调用的所有函数的列表。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/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.
Listing 9-2: The backtrace generated by a call to panic! displayed when the environment variable RUST_BACKTRACE is set

那是很多输出!你看到的确切输出可能因你的操作系统和 Rust 版本而异。为了获得带有这些信息的回溯,必须启用调试符号。调试符号在使用 cargo buildcargo run 时默认启用,除非指定了 --release 标志,就像我们这里一样。

在列表 9-2 的输出中,回溯的第 6 行指向了我们项目中导致问题的行:src/main.rs 的第 4 行。如果我们不希望程序崩溃,我们应该从第一个提到我们编写文件的行开始调查。在列表 9-1 中,我们故意编写了会导致崩溃的代码,修复崩溃的方法是不要请求超出向量索引范围的元素。当你的代码在未来崩溃时,你需要弄清楚代码正在使用哪些值执行什么操作导致崩溃,以及代码应该做什么来替代。

我们将回到“使用 panic! 还是不使用 panic! 部分,讨论何时应该和不应该使用 panic! 来处理错误条件。接下来,我们将看看如何使用 Result 从错误中恢复。