Rust: 错误处理

错误是软件中不可避免的事实,因此 Rust 具有许多功能来处理出现错误的情况。在许多情况下,Rust 要求您承认错误的可能性并在代码编译之前采取一些措施。此要求可确保您在将代码部署到生产环境之前发现错误并适当处理它们,从而使您的程序更加健壮!

Rust 将错误分为两大类:

  • 可恢复错误
    • 对于可恢复错误,它的类型为 Result<T, E>
  • 不可恢复错误
    • panic! 当程序遇到不可恢复的错误时停止执行的宏。

错误传播:?

0x01. 使用 panic! 处理不可恢复错误

展开堆栈或中止以响应恐慌

默认情况下,当发生恐慌时,程序会开始展开,这意味着 Rust 会回溯堆栈并清理它遇到的每个函数的数据。但是,回溯和清理工作非常繁重。因此,Rust 允许您选择立即中止的替代方案,这将结束程序而不进行清理。
如果您的项目需要使生成的二进制文件尽可能小,则可以通过在 Cargo.toml 文件中的相应 [profile] 部分中添加 panic = 'abort',从展开切换到在发生恐慌时中止。例如,如果您想在发布模式下在发生恐慌时中止,请添加以下内容:

1
2
[profile.release]
panic = 'abort'

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

1
2
3
fn main() {
panic!("crash and burn");
}

运行该程序时,你会看到如下内容:

1
2
3
4
5
6
7
8
$ 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

0x02. 使用 Result<T, E> 处理可恢复的错误

大多数错误并不严重到需要程序完全停止。有时,当某个函数失败时,其原因很容易解释和响应。例如,如果您尝试打开一个文件,并且由于该文件不存在而导致该操作失败,您可能希望创建该文件而不是终止该进程。

1
2
3
4
enum Result<T, E> {
Ok(T),
Err(E),
}

2.1 错误处理

1
2
3
4
5
6
7
8
9
10
use std::fs::File;

fn main() {
let greeting_file_result = File::open("hello.txt");

let greeting_file = match greeting_file_result {
Ok(file) => file,
Err(error) => panic!("Problem opening the file: {error:?}"),
};
}

2.2 匹配不同的错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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:?}"),
},
other_error => {
panic!("Problem opening the file: {other_error:?}");
}
},
};
}

优化错误处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
use std::fs::File;
use std::io::ErrorKind;

fn main() {
let greeting_file = File::open("hello.txt").unwrap_or_else(|error| {
if error.kind() == ErrorKind::NotFound {
File::create("hello.txt").unwrap_or_else(|error| {
panic!("Problem creating the file: {error:?}");
})
} else {
panic!("Problem opening the file: {error:?}");
}
});
}

2.3 错误处理的快捷方式:unwrapexpect

unwrap

1
2
3
4
5
use std::fs::File;

fn main() {
let greeting_file = File::open("hello.txt").unwrap();
}

如果我们在没有 hello.txt 文件的情况下运行此代码,我们将看到来自 unwrap 方法发出的 panic! 调用的错误消息:

1
2
thread 'main' panicked at src/main.rs:4:49:
called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }

expect
expect 方法还允许我们选择 panic! 错误消息。使用 expect 而不是 unwrap 并提供良好的错误消息可以传达您的意图,并使追踪 panic 的来源更加容易。expect 的语法如下所示:

1
2
3
4
5
6
use std::fs::File;

fn main() {
let greeting_file = File::open("hello.txt")
.expect("hello.txt should be included in this project");
}

expect 在调用 panic! 时使用的错误消息将是我们传递给 expect 的参数,而不是 unwrap 使用的默认 panic! 消息。它如下所示:

1
2
thread 'main' panicked at src/main.rs:5:10:
hello.txt should be included in this project: Os { code: 2, kind: NotFound, message: "No such file or directory" }

2.4 传播错误

当函数的实现调用了可能会失败的某个函数时,您可以将错误返回给调用代码,以便它决定要做什么,而不是在函数本身中处理错误。这称为传播错误,并为调用代码提供更多控制权,其中可能有更多信息或逻辑来指示应如何处理错误,而不是在代码上下文中可用的信息或逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
let username_file_result = File::open("hello.txt");

let mut username_file = match username_file_result {
Ok(file) => file,
Err(e) => return Err(e),
};

let mut username = String::new();

match username_file.read_to_string(&mut username) {
Ok(_) => Ok(username),
Err(e) => Err(e),
}
}

2.4.1 传播错误的快捷方式:? 运算符

1
2
3
4
5
6
7
8
9
use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
let mut username_file = File::open("hello.txt")?;
let mut username = String::new();
username_file.read_to_string(&mut username)?;
Ok(username)
}

2.4.2 在哪里可以使用 ? 运算符

? 运算符只能在返回类型与 ? 所用值兼容的函数中使用。这是因为 ? 运算符被定义为提前从函数中返回一个值。

是提前返回值,不是返回任意值

错误示例
应该返回 (),但是 ? 返回了 Result

1
2
3
4
5
use std::fs::File;

fn main() {
let greeting_file = File::open("hello.txt")?;
}

正确示例

1
2
3
4
# example 1
fn last_char_of_first_line(text: &str) -> Option<char> {
text.lines().next()?.chars().last()
}

幸运的是,main 函数也能返回 Result<(), E>。示例 9-12 的代码来自示例 9-10,但我们将 main 函数的返回类型改为 Result<(), Box<dyn Error>>,并在末尾添加了返回值 Ok(())。此代码现在可以通过编译。

1
2
3
4
5
6
7
8
9
# example 2
use std::error::Error;
use std::fs::File;

fn main() -> Result<(), Box<dyn Error>> {
let greeting_file = File::open("hello.txt")?;

Ok(())
}

当 main 函数返回 Result<(), E> 时,如果 main 返回 Ok(()),则可执行文件将以 0 值退出;如果 main 返回 Err 值,则可执行文件将以非零值退出。用 C 编写的可执行文件在退出时返回整数:成功退出的程序返回整数 0,出错的程序返回 0 以外的整数。Rust 还会从可执行文件中返回整数以兼容此约定。

0x03. To panic! or Not to panic!

那么你如何决定何时应该调用 panic! 以及何时应该返回 Result?当代码崩溃时,没有办法恢复。你可以针对任何错误情况调用 panic!,无论是否有可能恢复,但你代表调用代码决定这种情况是不可恢复的。当你选择返回 Result 值时,你为调用代码提供了选项。调用代码可以选择尝试以适合其情况的方式进行恢复,或者它可以决定在这种情况下 Err 值是不可恢复的,因此它可以调用 panic! 并将可恢复的错误变成不可恢复的错误。因此,当你定义可能失败的函数时,返回 Result 是一个很好的默认选择。

示例、原型代码和测试

  • 当您编写示例来说明某个概念时,同时包含强大的错误处理代码会使示例变得不那么清晰。
  • 同样,在您准备好决定如何处理错误之前,unwrap 和 expect 方法在原型设计时非常方便。
  • 如果测试中的方法调用失败,您会希望整个测试失败,即使该方法不是被测试的功能。因为 panic! 是将测试标记为失败的方式,所以调用 unwrap 或 expect 正是应该发生的事情。

你比编译器拥有更多信息的情况

如果您有其他逻辑可以确保 Result 具有 Ok 值,但编译器无法理解该逻辑,那么调用 unwrapexpect 也是合适的。您仍然需要处理 Result 值:无论您调用什么操作,通常仍有失败的可能性,即使在您的特定情况下逻辑上不可能。如果您可以通过手动检查代码来确保永远不会出现 Err 变体,那么调用 unwrap 是完全可以接受的,甚至最好在 expect 文本中记录您认为永远不会出现 Err 变体的原因。以下是一个例子:

1
2
3
4
5
use std::net::IpAddr;

let home: IpAddr = "127.0.0.1"
.parse()
.expect("Hardcoded IP address should be valid");