1. 简介

在很多情况下,Rust 要求你承认出错的可能性,并在编译代码之前就采取行动。这些要求使得程序更为健壮,它们确保了你会在将代码部署到生产环境之前就发现错误并正确地处理它们!Rust 将错误组合成两个主要类别:「可恢复错误」(recoverable)和「不可恢复错误」(unrecoverable)。

  • 可恢复错误通常代表向用户报告错误和重试操作是合理的情况,比如未找到文件。
  • 不可恢复错误通常是 bug 的同义词,比如尝试访问超过数组结尾的位置。

大部分语言并不区分这两类错误,并采用类似异常这样方式统一处理他们。Rust 并没有异常,但是有可恢复错误 Result<T, E> 和不可恢复(遇到错误时停止程序执行)错误 panic!

  • panic! 宏代表一个程序无法处理的状态,并停止执行而不是使用无效或不正确的值继续处理。
  • Result 枚举代表操作可能会在一种可以恢复的情况下失败。可以使用 Result 来告诉代码调用者他需要处理潜在的成功或失败。在

适当的场景使用 panic!Result 将会使代码在面对不可避免的错误时显得更加可靠。

2. panic! 与不可恢复错误

  • 当执行 panic! 宏时,程序会打印出一个错误信息,展开并清理栈数据,然后接着退出。出现这种情况的场景通常是检测到一些类型的 bug,而且程序员并不清楚该如何处理它。
1
2
3
fn main() {
panic!("crash and burn");
}

2.1 栈展开或终止

  • 当出现 panic 时,程序默认会开始「展开」(unwinding),这意味着 Rust 会回溯栈并清理它遇到的每一个函数的数据,不过这个回溯并清理的过程有很多工作。
  • 另一种选择是直接「终止」(abort),这会不清理数据就退出程序。那么程序所使用的内存需要由操作系统来清理。如果你需要项目的最终二进制文件越小越好,panic 时通过在 Cargo.toml[profile] 部分增加 panic = 'abort',可以由展开切换为终止。例如,如果你想要在 release 模式中 panic 时直接终止:
1
2
[profile.release]
panic = 'abort'

2.2 panic! 回溯

cargo run 时,我们可以设置 RUST_BACKTRACE=1 环境变量来回溯 panic! 清理过程 backtrace。Rust 的 backtrace 跟其他语言中的一样:阅读 backtrace 的关键是从头开始读直到发现你编写的文件,这就是问题的发源地。这一行往上是你的代码所调用的代码,往下则是调用你的代码的代码。这些行可能包含核心 Rust 代码,标准库代码或用到的 crate 代码。panic! 回溯输出可能因不同的操作系统和 Rust 版本而有所不同。

【注】为了获取带有这些信息的 backtrace,必须启用 debug 标识。当不使用 --release 参数运行 cargo buildcargo run 时 debug 标识会默认启用。

3. Result 与可恢复错误

大部分错误并没有严重到需要程序完全停止执行。有时,一个函数会因为一个容易理解并做出反应的原因失败。

3.1 Result 类型

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

其中,TE 是泛类型参数,T 代表成功时返回的 Ok 成员中的数据类型,而 E 代表失败时返回的 Err 成员中的错误类型。

3.2 匹配不同错误

以打开文件为例,有多种原因导致 File:open 失败。我们真正希望的是对不同的错误原因采取不同的行为:如果 File::open 因为文件不存在而失败,我们希望创建这个文件并返回新文件的句柄。如果 File::open 因为任何其他原因失败,例如没有打开文件的权限,则希望 panic!

File::open 返回的 Err 成员中的值类型 io::Error,它是一个标准库中提供的结构体。这个结构体有一个返回 io::ErrorKind 值的 kind 方法可供调用。io::ErrorKind 是一个标准库提供的枚举,它的成员对应 io 操作可能导致的不同错误类型。其中 ErrorKind::NotFound 代表尝试打开的文件并不存在。

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

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

let f = match f {
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),
},
};
}

3.3 panic! 简写

match 能够胜任它的工作,不过它可能有点冗长并且不总是能很好的表明其意图。Result<T, E> 类型定义了很多辅助方法来处理各种情况。

  • 其中一个方法为 unwrap,一下两种打开文件的处理方式等价:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use std::fs::File;

fn main() {
// 方式一
let f = File::open("hello.txt").unwrap();

// 方式二
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(error) => {
panic!("Problem opening the file: {:?}", error)
},
};
}
  • 另一个方法 expect 允许我们定义 panic! 的错误信息,使用 expect 而不是 unwrap 并提供一个好的错误信息可以表明你的意图并更易于追踪 panic 的根源。expect 的语法格式举例如下:
1
2
3
4
5
use std::fs::File;

fn main() {
let f = File::open("hello.txt").expect("Failed to open hello.txt");
}

【注】expectunwrap 的使用方式一样:返回函数成功调用的返回值或调用 panic! 宏。expect 用来调用 panic! 的错误信息将会作为参数传递给 expect,而不像 unwrap 那样使用默认的 panic! 信息。

3.4 传播错误

当编写一个其实现会调用一些可能会失败的操作的函数时,除了在这个函数中处理错误外,还可以选择让调用者知道这个错误并决定该如何处理。这被称为「传播」(propagating)错误,这样能更好的控制代码调用,因为比起你代码所拥有的上下文,调用者可能拥有更多信息或逻辑来决定应该如何处理错误。

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

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

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

let mut s = String::new();

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

除了可以使用上述例子的繁琐 match 分支来实现传播错误外,Rust 还提供了 ? 运算符用于简写传播错误。Result 值之后的 ? 的作用是:

  • 如果 Result 的值是 Ok,这个表达式将会返回 Ok 中的值而程序将继续执行。
  • 如果 Result 的值是 ErrErr 中的值将作为整个函数的返回值,就好像使用了 return 关键字一样,这样错误值就被传播给了调用者。
1
2
3
4
5
6
7
8
9
10
use std::io;
use std::io::Read;
use std::fs::File;

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

甚至可以在 ? 之后直接使用链式方法调用来进一步缩短代码:

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

fn read_username_from_file() -> Result<String, io::Error> {
let mut s = String::new();

File::open("hello.txt")?.read_to_string(&mut s)?;

Ok(s)
}

【注】? 运算符所使用的错误值被传递给了 from 函数,它定义于标准库的 From trait 中,其用来将错误从一种类型转换为另一种类型。当 ? 运算符调用 from 函数时,收到的错误类型将被转换为由当前函数返回类型所指定的错误类型。

5. 错误处理指导原则

5.1 使用 panic!

在当有可能会导致有害状态的情况下建议使用 panic! —— 在这里,有害状态是指当一些假设、保证、协议或不可变性被打破的状态,例如无效的值、自相矛盾的值或者被传递了不存在的值等,外加如下几种情况:

  • 有害状态并不包含预期会偶尔发生的错误。
  • 在此之后代码的运行依赖于不处于这种有害状态。
  • 当没有可行的手段来将有害状态信息编码进所使用的类型中的情况。

5.2 使用 Result

当错误预期会出现时,返回 Result 要比调用 panic! 更为合适。这样的例子包括解析器接收到格式错误的数据,或者 HTTP 请求返回了一个表明触发了限流的状态。在这些例子中,应该通过返回 Result 来表明失败预期是可能的,这样将有害状态向上传播,调用者就可以决定该如何处理这个问题。使用 panic! 来处理这些情况就不是最好的选择。

4. 具体错误

4.1 mismatched types

该错误信息表示代码中出现了「类型不匹配」。除了 Rust 中已定义的数据类型外,错误信息中还会使用空元组 () 来表示空类型。即使用空元组 () 表示没有返回值。