开发环境
- Windows 10
- Rust 1.66.1
- VS Code 1.74.3
项目工程
这里继续沿用上次工程rust-demo
错误处理
错误是软件生活中的一个事实,所以Rust有一些处理出错情况的功能。在许多情况下,Rust要求你承认错误的可能性,并在你的代码编译前采取一些行动。这一要求使你的程序更加健壮,因为它可以确保你在将代码部署到生产中之前就能发现错误并进行适当的处理。
Rust将错误分为两大类:可恢复的和不可恢复的错误。对于一个可恢复的错误,比如文件未找到的错误,我们很可能只想向用户报告问题并重试操作。不可恢复的错误总是错误的症状,比如试图访问一个超过数组末端的位置,因此我们要立即停止程序。
大多数语言不区分这两种错误,并以同样的方式处理这两种错误,使用异常等机制。Rust没有异常。相反,它有Result<T, E>类型,用于处理可恢复的错误,还有panic!宏,在程序遇到不可恢复的错误时停止执行。本章首先涉及调用panic!,然后讨论返回Result<T, E>值。此外,我们将探讨在决定是尝试从错误中恢复还是停止执行时的注意事项。
无法恢复的错误:panic!
有时,在你的代码中会发生不好的事情,而你却无能为力。在这种情况下,Rust有panic!宏。在实践中,有两种方法可以导致恐慌:通过采取导致我们的代码恐慌的行动(比如访问一个超过终点的数组),或者明确地调用panic!宏。在这两种情况下,我们都会在我们的程序中引起恐慌。默认情况下,这些恐慌会打印一个失败的消息,解开,清理堆栈,然后退出。通过环境变量,你也可以让Rust在恐慌发生时显示调用堆栈,以便更容易追踪到恐慌的来源。
解除堆栈或终止对恐慌的反应
默认情况下,当恐慌发生时,程序会开始解开,这意味着Rust会向后移动堆栈,清理它遇到的每个函数的数据。然而,这种回走和清理是一个很大的工作。因此,Rust允许你选择立即中止,即在不进行清理的情况下结束程序。
程序所使用的内存将需要由操作系统来清理。如果在你的项目中,你需要使生成的二进制文件尽可能的小,你可以通过在Cargo.toml文件中适当的[profile]部分添加panic = 'abort',将解卷转换为恐慌时中止。例如,如果你想在发布模式下,在恐慌时中止,请添加这个。
[profile.release] panic = 'abort'
让我们试着在一个简单的程序中调用panic!
文件名: src/main.rs
fn main() {
panic!("crash and burn"); // panic!宏
}
运行
cargo run
对panic!的调用导致了最后两行中包含的错误信息。第一行显示了我们的恐慌信息和源代码中发生恐慌的地方: src/main.rs:914:5 表示这是我们 src/main.rs 文件的第二行,第五个字符。
在这种情况下,指示的行是我们代码的一部分,如果我们去看那一行,我们就会看到panic!宏的调用。在其他情况下,panic!调用可能在我们的代码所调用的代码中,而错误信息所报告的文件名和行号将是别人的代码,其中调用了panic!宏,而不是我们的代码中最终导致panic!调用的那一行。我们可以使用panic!调用所来自的函数的回溯,来找出我们代码中导致问题的部分。接下来我们将更详细地讨论回溯。
使用panic! 溯源
让我们看看另一个例子,看看当panic!调用来自于一个库,因为我们的代码中有一个bug,而不是来自于我们的代码直接调用宏时,会是什么样子。下例中有一些代码试图访问一个向量中超出有效索引范围的索引。
文件名: src/main.rs
fn main() {
let v = vec![1, 2, 3];
v[99]; // 向量越界访问
}
在这里,我们试图访问我们的向量的第100个元素(它在索引99处,因为索引从0开始),但向量只有3个元素。在这种情况下,Rust会出现恐慌错误。使用[]应该返回一个元素,但是如果你传递了一个无效的索引,那么Rust在这里就没有任何元素可以正确返回。
在C语言中,试图读取一个数据结构的末端是未定义的行为。你可能会得到内存中对应于数据结构中该元素的任何位置,即使该内存不属于该结构。这被称为缓冲区超读,如果攻击者能够以这样的方式操纵索引,从而读取他们不应该被允许的、存储在数据结构之后的数据,就会导致安全漏洞。
为了保护你的程序不受这种漏洞的影响,如果你试图在一个不存在的索引上读取一个元素,Rust会停止执行并拒绝继续。
运行
cargo run
这个错误指向main.rs的第920行,在那里我们试图访问索引99。 下一行注释告诉我们,我们可以设置RUST_BACKTRACE环境变量,以获得导致错误的确切情况的回溯。回溯是一个所有被调用的函数的列表,以达到这一点。Rust中的回溯和其他语言中的回溯一样:阅读回溯的关键是从顶部开始阅读,直到看到你写的文件。这就是问题的根源所在。在这个位置上面的行是你的代码所调用的代码;下面的行是调用你的代码的代码。这些前后的行可能包括Rust核心代码、标准库代码或你正在使用的crates。让我们尝试通过设置RUST_BACKTRACE环境变量为0以外的任何值来获取回溯。
使用下述指令运行
RUST_BACKTRACE=1 cargo run
这是个很多的输出! 你看到的确切输出可能会有所不同,这取决于你的操作系统和Rust版本。为了获得这些信息的回溯,必须启用调试符号。当使用cargo build或cargo run而不使用--release标志时,调试符号是默认启用的,就像我们这里的情况。
在上图的输出中,回溯的第6行指向我们项目中导致问题的那一行: src/main.rs的第920行。如果我们不想让我们的程序恐慌,我们应该从提到我们所写的文件的第一行所指向的位置开始调查。在本节第一个示例中,我们故意写了会恐慌的代码,解决恐慌的方法是不要请求超出向量索引范围的元素。当你的代码在未来出现恐慌时,你需要弄清楚代码用什么值做了什么动作来引起恐慌,以及代码应该做什么来代替。
我们将在本章后面的 "恐慌!或不恐慌!"一节中再来讨论painc!以及何时应该和不应该使用painc!来处理错误情况。
本章重点
- 错误处理的概念
- Rust语言中错误处理的painc!宏
- Rust恐慌朔源以及如何使用