[!|center] 普若哥们儿
错误处理
Rust 将错误分为两大类:可恢复的(recoverable)和 不可恢复的(unrecoverable)错误。对于一个可恢复的错误,比如文件未找到的错误,我们很可能只想向用户报告问题并重试操作。不可恢复的错误总是 bug 出现的征兆,比如试图访问一个超过数组末端的位置,因此我们要立即停止程序。
Rust 用Result<T, E> 类型处理可恢复的错误,用 panic! 宏处理不可恢复错误。
用 panic! 宏处理不可恢复的错误
有两种情况造成 panic:执行会造成 panic 的操作(比如访问超过数组结尾的内容)或者显式调用 panic! 宏。这两种情况都会使程序 panic,通常情况下这些 panic 会打印出一个错误信息,展开并清理栈数据,然后退出。通过设置环境变量 RUST_BACKTRACE=1,可以让 Rust 在 panic 发生时打印调用堆栈(call stack)以便于定位 panic 的原因。
当出现 panic 时,程序默认会开始 展开(unwinding),这意味着 Rust 会回溯栈并清理它遇到的每一个函数的数据,不过这个回溯并清理的过程有很多工作,比如调用操作系统清理内存。另一种选择是直接 终止(abort),这会不清理数据就退出程序。
如果希望项目的最终二进制文件越小越好,panic 时通过在 Cargo. toml 的 [profile] 部分增加 panic = 'abort',可以由展开切换为终止。例如,如果你想要在 release 模式中 panic 时直接终止:
> [profile.release]
> panic = 'abort'
下例在程序中调用 panic!:
fn main() {
panic!("crash and burn");
}
运行程序将会出现类似这样的输出:
thread 'main' panicked at 'crash and burn', src/main.rs:2:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
最后两行包含 panic! 调用造成的错误信息。第一行显示了 panic 提供的信息并指明了源码中 panic 出现的位置:src/main.rs: 2:5 表明这是 src/main.rs 文件的第二行第五个字符。
在这个例子中,被指明的那一行是我们代码的一部分,而且查看这一行的话就会发现 panic! 宏的调用。在其他情况下,panic! 可能会出现在我们的代码所调用的代码中,错误信息报告的文件名和行号可能指向别人代码中的 panic! 宏调用,而不是我们代码中最终导致 panic! 的那一行。可以设置设置环境变量 RUST_BACKTRACE=1,让 Rust 在 panic 发生时打印调用堆栈(call stack)来寻找代码中出问题的地方。
使用 panic! 的 backtrace
让我们来看看另一个因为我们代码中的 bug 引起的别的库中 panic! 的例子,而不是直接的宏调用。下例有一些尝试通过索引访问 vector 中元素的例子:
fn main() {
let v = vec![1, 2, 3];
v[99];
}
这里尝试访问 vector 索引为 99 的元素,不过它只有三个元素,索引是无效的,这种情况下 Rust 会 panic。
C 语言中,尝试读取数据结构之后的值是未定义行为(undefined behavior)。你会得到任何对应数据结构中这个元素的内存位置的值,甚至是这些内存并不属于这个数据结构的情况。这被称为 缓冲区溢出(buffer overread),并可能会导致安全漏洞,比如攻击者可以像这样操作索引来读取储存在数据结构之后不被允许的数据。
为了保护程序远离这类漏洞,如果尝试读取一个索引不存在的元素,Rust 会停止执行并拒绝继续。尝试运行上面的程序会出现如下:
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:4:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
编译器提示可以设置环境变量 RUST_BACKTRACE=1 来显示 backtrace。backtrace 是一个执行到目前位置所有被调用的函数的列表。Rust 的 backtrace 跟其他语言中的一样:阅读 backtrace 的关键是从头开始读直到发现你编写的源文件,这就是问题的发源地。这一行往上是你的代码所调用的代码;往下则是调用你的代码的代码。这些行可能包含核心 Rust 代码,标准库代码或用到的 crate 代码。让我们将 RUST_BACKTRACE 环境变量设置为任何不是 0 的值来获取 backtrace 看看。下面展示了与你看到类似的输出:
$ RUST_BACKTRACE=1 cargo run
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:4:5
stack backtrace:
0: rust_begin_unwind
at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/std/src/panicking.rs:584:5
1: core::panicking::panic_fmt
at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/panicking.rs:142:14
...
6: panic::main
at ./src/main.rs:4:5
7: core::ops::function::FnOnce::call_once
at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/ops/function.rs:248:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
为了获取输出 backtrace,必须启用 debug 标识。当不使用 --release 参数运行 cargo build 或 cargo run 时 debug 标识会默认启用。
用 Result 处理可恢复的错误
有些错误是可恢复的,例如,如果因为打开一个并不存在的文件而失败,此时我们可能想要创建这个文件,而不是终止进程。
标准库中的 Result 枚举类型定义有如下两个成员,Ok 和 Err:
enum Result<T, E> {
Ok(T),
Err(E),
}
T 和 E 是泛型类型参数。T 代表成功时返回的 Ok 成员中的数据的类型,而 E 代表失败时返回的 Err 成员中的错误的类型。
下例代码打开一个文件:
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),
};
}
File::open 的返回值是 Result<T, E>。泛型参数 T 会被 File::open 的实现放入成功返回值的类型 std::fs::File,这是一个文件句柄。错误返回值使用的 E 的类型是 std::io::Error。
当 File::open 成功时,greeting_file_result 变量将会是一个包含文件句柄的 Ok 实例。当失败时,greeting_file_result 变量将会是一个包含了更多关于发生了何种错误的信息的 Err 实例,我们需要根据 File::open 返回值进行不同处理。
注意与 Option 枚举一样,Result 枚举和其成员也被导入到了 prelude 中,所以就不需要在 match 分支中的 Ok 和 Err 之前指定 Result::。
match 语句结果是 Ok 时,返回 Ok 成员中的 file 值,然后将这个文件句柄赋值给变量 greeting_file,可以利用这个文件句柄来进行读写。
match 的另一个分支处理从 File::open 得到 Err 值的情况。在这种情况下,调用了 panic! 宏。如果当前目录没有一个叫做 hello.txt 的文件,当运行这段代码时会看到如下来自 panic! 宏的输出:
thread 'main' panicked at 'Problem opening the file: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:8:23
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
匹配不同的错误
上例中的代码不管 File::open 是因为什么原因失败都会 panic!。我们希望对不同的错误原因采取不同的行为:如果 File::open 因为文件不存在而失败,我们希望创建这个文件并返回新文件的句柄。如果 File::open 因为任何其他原因失败,例如没有打开文件的权限,则panic!。让我们看看下例,其中 match 增加了另一个分支:
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);
}
},
};
}
File::open 返回的 Err 成员中的值类型 io::Error,它是一个标准库中提供的结构体。这个结构体有一个返回 io::ErrorKind 值的 kind 方法可供调用。io::ErrorKind 是一个标准库提供的枚举,它的成员对应 io 操作可能导致的不同错误类型。我们感兴趣的成员是 ErrorKind::NotFound,它代表尝试打开的文件并不存在。这样,match 就匹配完 greeting_file_result 了,不过对于 error.kind() 还有一个内层 match。
我们希望在内层 match 中检查的条件是 error.kind() 的返回值是否为 ErrorKind 的 NotFound 成员。如果是,则尝试通过 File::create 创建文件。然而因为 File::create 也可能会失败,还需要增加一个内层 match 语句。当文件不能被创建,会打印出一个不同的错误信息。外层 match 的最后一个分支保持不变,这样对任何除了文件不存在的错误会使程序 panic。
失败时 panic 的简写:unwrap 和 expect
Result<T, E> 类型定义了很多辅助方法来处理各种情况。其中之一叫做 unwrap,它的实现就类似于match 语句。如果 Result 值是成员 Ok,unwrap 会返回 Ok 中的值。如果 Result 是成员 Err,unwrap 会为我们调用 panic!。
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt").unwrap();
}
如果调用这段代码时不存在 hello.txt 文件,我们将会看到一个 unwrap 调用 panic! 时提供的错误信息:
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os {
code: 2, kind: NotFound, message: "No such file or directory" }',
src/main.rs:4:49
expect 与 unwrap 的使用方式一样:返回文件句柄或调用 panic! 宏。expect 在调用 panic! 时使用的错误信息将是我们传递给 expect 的参数,而不像 unwrap 那样使用默认的 panic! 信息。
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt")
.expect("hello.txt should be included in this project");
}
输出:
thread 'main' panicked at 'hello.txt should be included in this project: Error
{ repr: Os { code: 2, message: "No such file or directory" } }',
src/libcore/result.rs:906:4
在生产级别的代码中,大部分 Rustaceans 选择 expect 而不是 unwrap 并提供更为有意义的错误信息。
传播错误
一个函数中的错误还可以选择让调用者知道这个错误并决定该如何处理,这被称为 传播(propagating)错误,这样能更好的控制程序的运行,因为调用者可能拥有更多信息或逻辑来决定应该如何处理错误。
下例展示了一个从文件中读取用户名的函数,如果文件不存在或读取不到用户名,这个函数会将这些错误返回给调用它的代码:
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),
}
}
fn main() {
let username = read_username_from_file().expect("Unable to get username");
}
函数 read_username_from_file 的返回值为 Result<String, io::Error>,这意味着函数返回一个 Result<T, E> 类型的值,其中泛型参数 T 的具体类型是 String,而 E 的具体类型是 io::Error。
如果这个函数没有出任何错误成功返回,函数的调用者会收到一个包含 String 的 Ok 值 —— 函数从文件中读取到的用户名。如果 File::open 函数或 read_to_string 方法调用失败,则返回 io::Error 类型的 Err 值。
第 9 行,如果 File::open 打开文件失败,则使用 return 关键字提前结束整个函数,并将来自 File::open 的错误值(现在在模式变量 e 中)作为函数的错误值传回给调用者。
第 16 行,如果 read_to_string 执行失败,则将 read_to_string 返回的错误值返回给函数的调用者,这里无需显式调用 return 语句,因为这是函数的最后一个表达式。
main 函数中调用 read_username_from_file 函数的代码最终会得到一个包含用户名的 Ok 值,或者一个包含 io::Error 的 Err 值。调用者可以自行决定如何处理函数返回的错误,可以选择 panic! 使程序崩溃,或者使用一个默认的用户名。函数内部如果难以确定如何处理逻辑,可以将所有的成功或失败信息向上传播,让调用者选择合适的处理方法。
传播错误的简写:? 运算符
下例展示了一个 read_username_from_file 的实现,它实现了与上例的代码相同的功能,不过这个实现使用了 ? 运算符:
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)
}
fn main() {
let username = read_username_from_file().expect("Unable to get username");
}
File::open 的返回值是 Result{Ok,Err} 类型,? 运算符实际上是作用在 Result 类型上,是针对 Result 类型调用 ? 运算符。如果 File::open 调用成功, ? 运算符会将 Ok 中的值返回给变量 username_file;如果发生了错误,? 运算符会使整个函数提前返回,并将任何 Err 值返回给调用代码。同理 read_to_string 调用结尾 ? 运算符也是这样。
? 运算符消除了大量样板代码并使得函数的实现更简单。我们甚至可以在 ? 之后直接使用链式方法调用来进一步缩短代码,如下例所示:
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let mut username = String::new();
File::open("hello.txt")?.read_to_string(&mut username)?;
Ok(username)
}
fn main() {
let username = read_username_from_file().expect("Unable to get username");
}
示例对 File::open("hello.txt")? 的结果直接链式调用了 read_to_string,而不再创建变量 username_file。当 File::open 和 read_to_string 都成功没有失败时返回包含用户名 username 的 Ok 值,其功能之前保持一致。
[!note] 将文件读取到一个字符串是相当常见的操作,所以 Rust 提供了名为
fs::read_to_string的函数,它会打开文件、新建一个String、读取文件的内容,并将内容放入String,接着返回它。下例展示了使用fs::read_to_string的更为简短的写法:use std::fs; use std::io; fn read_username_from_file() -> Result<String, io::Error> { fs::read_to_string("hello.txt") } fn main() { let username = read_username_from_file().expect("Unable to get username"); }
哪里可以使用 ? 运算符
? 运算符只能被用于返回值为 Result 或 Option 以及任何实现了 FromResidual trait的类型的函数。
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt")?;
}
? 运算符作用于 File::open 返回的 Result 值,而 main 函数的返回类型是 () ,而不是 Result。当编译这些代码会报错。
为了修复这个错误,有两个选择。一个是,如果没有限制的话将 main 函数的返回值改为 Result<T, E>。另一个是在 main 函数中使用 match 或 Result<T, E> 的方法来处理 File::open 的返回值 Result<T, E> ,而不是向上传播。
错误信息也提到 ? 也可用于 Option<T> 值。如同对 Result 使用 ? 一样,只能在返回 Option 的函数中对 Option 使用 ?。在 Option<T> 上调用 ? 运算符的行为与 Result<T, E> 类似:如果值是 None,此时 None 会从函数中提前返回。如果值是 Some,Some 中的值作为表达式的返回值同时函数继续。下例有一个从给定文本中返回第一行最后一个字符的函数的例子:
fn last_char_of_first_line(text: &str) -> Option<char> {
text.lines().next()?.chars().last()
}
fn main() {
assert_eq!(
last_char_of_first_line("Hello, world\nHow are you today?"),
Some('d')
);
assert_eq!(last_char_of_first_line(""), None);
assert_eq!(last_char_of_first_line("\nhi"), None);
}
函数 last_char_of_first_line 返回值类型为 Option<char> ,如果 text 是空字符串,next 会返回 None,此时 ? 运算符停止后续流程并从 last_char_of_first_line 返回 None。如果 text 不是空字符串,next 会返回一个包含 text 中第一行的字符串 slice 的 Some 值。
注意你可以在返回 Result 的函数中对 Result 使用 ? 运算符,可以在返回 Option 的函数中对 Option 使用 ? 运算符,但是不可以混合搭配。? 运算符不会自动将 Result 转化为 Option,反之亦然;在这些情况下,可以使用类似 Result 的 ok 方法或者 Option 的 ok_or 方法来显式转换。
main 函数返回值
目前为止,我们所使用的所有 main 函数都返回 ()。main 函数是特殊的因为它是可执行程序的入口点和退出点,为了使程序能正常工作,其可以返回的类型是有限制的。
幸运的是 main 函数也可以返回 Result<(), E>,下例中的代码修改了 main 的返回值为 Result<(), Box<dyn Error>> 并在结尾增加了一个 Ok(()) 作为返回值。这段代码可以编译:
use std::error::Error;
use std::fs::File;
fn main() -> Result<(), Box<dyn Error>> {
let greeting_file = File::open("hello.txt")?;
Ok(())
}
Box<dyn Error> 类型是一个 trait 对象(trait object)。可以将 Box<dyn Error> 理解为 “任何类型的错误”。在返回 Box<dyn Error> 错误类型 main 函数中对 Result 使用 ? 是允许的,因为它允许任何 Err 值提前返回。即便 main 函数体从来只会返回 std::io::Error 错误类型,通过指定 Box<dyn Error>,这个签名也仍是正确的,甚至当 main 函数体中增加更多返回其他错误类型的代码时也是如此。
当 main 函数返回 Result<(), E>,如果 main 返回 Ok(()) 可执行程序会以 0 值退出,而如果 main 返回 Err 值则会以非零值退出;成功退出的程序会返回整数 0,运行错误的程序会返回非 0 的整数。Rust 也会从二进制程序中返回与这个惯例相兼容的整数。
main 函数也可以返回任何实现了std::process::Termination trait的类型,它包含了一个返回 ExitCode 的 report 函数。请查阅标准库文档了解更多为自定义类型实现 Termination trait 的细节。