
7 错误处理
使用 Result<T, E> 类型来处理可恢复错误,使用 panic! 宏在程序遇到不可恢复错误时停止执行。
不可恢复的错误: panic!
实际中有两种方式会导致 panic:
一种是执行了会让代码 panic 的操作,比如访问超出数组结尾的位置;
另一种是显式调用panic! 宏。
这些 panic 会打印失败信息、展开栈、清理栈数据,然后退出。你还可以通过环境变量,让 Rust 在 panic 发生时显示调用栈(call stack),以便更容易追踪 panic 的来源。
那么程序所使用的内存需要由操作系统来清理。如果你需要项目的最终二进制文件越小越好,panic 时通过在 Cargo.toml 的 [profile] 部分增加 panic = 'abort',可以由展开切换为终止。例如,如果你想要在 release 模式中 panic 时直接终止,可添加:
[profile.release] panic = 'abort'
panic信息
$ 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
backtrace回溯堆栈
设置RUST_BACKTRACE 环境变量来得到 backtrace。backtrace 是一份到达当前执行点之前所有被调用函数的列表。
要获得带有这些信息的 backtrace,必须启用调试符号(debug symbols)。
# powershell
$env:RUST_BACKTRACE=1; cargo run
# bash shell
RUST_BACKTRACE=1 cargo run
# windows cmd
set RUST_BACKTRACE=1 && cargo run
不带 --release 参数运行 cargo build 或 cargo run 时,调试符号默认就是启用的。
用Result处理可恢复的错误
Result 枚举,它定义有如下两个变体,Ok 和 Err,可以使用模式匹配判断结果类型。并从中获得详细信息。
enum Result<T, E> { Ok(T), Err(E), }
非常多的库中的方法返回的值就是Result<T, E>类型。
Error类型
这个结构体有一个返回io::ErrorKind 值的 kind 方法可供调用。io::ErrorKind 是一个标准库提供的枚举,它的变体对应 io 操作可能导致的不同错误类型。
我们可以借Error中的kind()方法获得,这个error的错误类型:
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let greeting_file_result = File::open("hello.txt");
let greeting_file = match greeting_file_result {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Problem creating the file: {e:?}"),
},
_ => {
panic!("Problem opening the file: {error:?}");
}
},
};
}
Result提供的快捷处理异常方法
如果枚举不是OK变体,那么调用wnwrap会直接调用panic!如果是OK变体则正常返回内部类型。
另外还有expect方法比unwrap多了一个异常提示信息的参数,行为与unwrap类似。
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt")
.expect("hello.txt should be included in this project");
}
在生产级别的代码中,大部分 Rustaceans 选择 expect 而不是 unwrap 并提供更多关于为何操作期望是一直成功的上下文。如此如果该假设真的被证明是错的,你也有更多的信息来用于调试。
当你有某些其他逻辑来确保Result 一定会是 Ok 值,而编译器又无法理解这套逻辑时,调用 expect 也是合适的。同时最好在 expect 的提示文本里记录下你为什么认为这里永远不会出现 Err。
[!summary] 使用Result的.map_error方法来转换错误类型,将一个错误类型包装成统一错误
经常出现的问题就是,错误类型不统一,但这些错误都在一个栈帧中,返回result的错误类型只有一种,这个时候就能使用.map_error将错误转换同类错误类型。
传播错误
所有可能出现错误环节返回值都会是Result,有OK与Err两种变体。
这种传播错误的模式在 Rust 中太常见了,因此 Rust 提供了问号运算符 ? 来简化这一过程。
放在 Result 值后面的 ?,其定义的工作方式与unwrap非常的像,但是当Result的值为Err的时候,不再是调用 panic! 而是 return 这个Err(,这样错误值就被传播给了调用者。
但是无法将Result转换成Option,因为结构完全不同,错误信息有可能在Option中完全没有地方放。
? 运算符消除了大量样板代码并使得函数的实现更简单。我们甚至可以在 ? 之后直接使用链式方法调用来进一步简化代码。
幸运的是 main 函数也可以返回 Result<(), E>。如果 main 返回 Ok(()) 可执行程序会以 0 值退出,而如果 main 返回 Err 值则会以非零值退出;
main 函数也可以返回任何实现了 std::process::Termination trait 的类型,它包含了一个返回 ExitCode 的 report 函数。
panic!使用建议
1 快速测试、demo、原型等
在我们准备好决定如何处理错误之前,unwrap和expect方法在原型设计时非常方便。当我们准备好让程序更加健壮时,它们会在代码中留下清晰的标记。
如果方法调用在测试中失败了,我们希望这个测试都失败,即便这个方法并不是需要测试的功能。因为 panic! 会将测试标记为失败,此时调用 unwrap 或 expect 是恰当的。
2 当你确定不会出错时
当你有某些其他逻辑来确保 Result 一定会是 Ok 值,而编译器又无法理解这套逻辑时,调用 expect 也是合适的。
同时最好在 expect 的提示文本里记录下你为什么认为这里永远不会出现 Err。
在 expect 中写明为什么不认为会出错的原因,也会提醒我们:如果将来原因不存在了,就该把 expect 改成更合适的错误处理代码。
3 当错误出现于调用方
函数通常都遵循契约(contracts):它们的行为只有在输入满足特定条件时才能得到保证。
当违反契约时 panic 是有道理的,因为这通常代表调用方的 bug,而且这也不是那种你希望所调用的代码必须处理的错误。事实上所调用的代码也没有合理的方式来恢复,而是需要调用方的开发者修复其代码。函数的契约,尤其是当违反它会造成 panic 的契约,应该在函数的 API 文档中进行说明。
