Skip to main content
7 错误处理

7 错误处理

•1 min•205 words
Summary

使用 Result<T, E> 类型来处理可恢复错误,使用 panic! 宏在程序遇到不可恢复错误时停止执行。

不可恢复的错误: panic!

虽然我们无法处理这些错误,但仍然想要弄清楚原因

实际中有两种方式会导致 panic:
一种是执行了会让代码 panic 的操作,比如访问超出数组结尾的位置;
另一种是显式调用panic! 宏。

这些 panic 会打印失败信息、展开栈、清理栈数据,然后退出。你还可以通过环境变量,让 Rust 在 panic 发生时显示调用栈(call stack),以便更容易追踪 panic 的来源。

Todo

那么程序所使用的内存需要由操作系统来清理。如果你需要项目的最终二进制文件越小越好,panic 时通过在 Cargo.toml 的 [profile] 部分增加 panic = 'abort',可以由展开切换为终止。例如,如果你想要在 release 模式中 panic 时直接终止,可添加:

[profile.release] panic = 'abort'

panic信息

Summary
$ 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
Warning
Todo

backtrace回溯堆栈

通过香环境变量打开rust的回溯堆栈打印

设置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类型

通常标准库中Result中的Err变体中存放的类型是IO::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提供的快捷处理异常方法

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 并提供更多关于为何操作期望是一直成功的上下文。如此如果该假设真的被证明是错的,你也有更多的信息来用于调试。

其实调用panic!以及unwrap和except是一个很好的消除Result办法

当你有某些其他逻辑来确保Result 一定会是 Ok 值,而编译器又无法理解这套逻辑时,调用 expect 也是合适的。同时最好在 expect 的提示文本里记录下你为什么认为这里永远不会出现 Err。

使用Result的方法来转换错误类型,将一个错误类型包装成统一错误

[!summary] 使用Result的.map_error方法来转换错误类型,将一个错误类型包装成统一错误
经常出现的问题就是,错误类型不统一,但这些错误都在一个栈帧中,返回result的错误类型只有一种,这个时候就能使用.map_error将错误转换同类错误类型。

传播错误

Summary

所有可能出现错误环节返回值都会是Result,有OK与Err两种变体。
这种传播错误的模式在 Rust 中太常见了,因此 Rust 提供了问号运算符 ? 来简化这一过程。

放在 Result 值后面的 ?,其定义的工作方式与unwrap非常的像,但是当Result的值为Err的时候,不再是调用 panic! 而是 return 这个Err(,这样错误值就被传播给了调用者。

Warning
Todo

但是无法将Result转换成Option,因为结构完全不同,错误信息有可能在Option中完全没有地方放。

? 运算符消除了大量样板代码并使得函数的实现更简单。我们甚至可以在 ? 之后直接使用链式方法调用来进一步简化代码。

Todo

幸运的是 main 函数也可以返回 Result<(), E>。如果 main 返回 Ok(()) 可执行程序会以 0 值退出,而如果 main 返回 Err 值则会以非零值退出;
main 函数也可以返回任何实现了 std::process::Termination trait 的类型,它包含了一个返回 ExitCode 的 report 函数。

panic!使用建议

1 快速测试、demo、原型等

Summary

在我们准备好决定如何处理错误之前,unwrap和expect方法在原型设计时非常方便。当我们准备好让程序更加健壮时,它们会在代码中留下清晰的标记。

如果方法调用在测试中失败了,我们希望这个测试都失败,即便这个方法并不是需要测试的功能。因为 panic! 会将测试标记为失败,此时调用 unwrap 或 expect 是恰当的。

2 当你确定不会出错时

Summary

当你有某些其他逻辑来确保 Result 一定会是 Ok 值,而编译器又无法理解这套逻辑时,调用 expect 也是合适的。

同时最好在 expect 的提示文本里记录下你为什么认为这里永远不会出现 Err。

在 expect 中写明为什么不认为会出错的原因,也会提醒我们:如果将来原因不存在了,就该把 expect 改成更合适的错误处理代码。

3 当错误出现于调用方

Summary

函数通常都遵循契约(contracts):它们的行为只有在输入满足特定条件时才能得到保证。

当违反契约时 panic 是有道理的,因为这通常代表调用方的 bug,而且这也不是那种你希望所调用的代码必须处理的错误。事实上所调用的代码也没有合理的方式来恢复,而是需要调用方的开发者修复其代码。函数的契约,尤其是当违反它会造成 panic 的契约,应该在函数的 API 文档中进行说明。