❝作出决策就是要求我们在一个目标与另一个目标直接进行权衡取舍❞
大家好,我是「柒八九」。
今天,我们继续「Rust学习笔记」的探索。我们来谈谈关于「错误处理」的相关知识点。
如果,想了解该系列的文章,可以参考我们已经发布的文章。如下是往期文章。
❝
好了,天不早了,干点正事哇。

在很多情况下,Rust 要求你承认出错的可能性,并在编译代码之前就采取行动。这些要求使得程序更为健壮,它们确保了你会在将代码部署到生产环境之前就发现错误并正确地处理它们!
Rust 将错误组合成两个主要类别:可恢复错误recoverable和 不可恢复错误unrecoverable。
bug 的同义词,比如尝试访问超过数组结尾的位置。有的时候代码出问题了,而你对此束手无策。对于这种情况,Rust 有 panic!宏。当执行这个宏时,程序会打印出一个错误信息,展开并清理栈数据,然后接着退出。出现这种情况的场景通常是检测到一些类型的 bug,而且开发者并不清楚该如何处理它。
❝当出现
panic时,程序默认会开始 展开unwinding,这意味着Rust会回溯栈并清理它遇到的每一个函数的数据,不过这个回溯并清理的过程有很多工作。 另一种选择是直接 终止abort,这会不清理数据就退出程序。那么程序所使用的内存需要「由操作系统来清理」。 ❞
如果你需要项目的最终二进制文件越小越好,panic 时通过在 Cargo.toml 的 [profile] 部分增加 panic = 'abort',可以由展开切换为终止。
[profile.release]
panic = 'abort'在一个简单的程序中调用 panic!:
fn main() {
panic!("crash and burn");
}运行程序将会出现类似这样的输出:

两行包含 panic! 调用造成的错误信息。
panic 提供的信息并指明了源码中 panic 出现的位置:src/main.rs:2:5 表明这是 src/main.rs 文件的第二行第五个字符。来看看另一个因为我们代码中的 bug 引起的别的库中 panic! 的例子,而不是直接的宏调用。
fn main() {
let v = vec![1, 2, 3];
v[99];
}
尝试访问 vector 的第 100 个元素,不过它只有 3 个元素。这种情况下 Rust 会 panic。[] 应当返回一个元素,但是如果传递了一个无效的索引,那么 Rust 在这里返回任何元素都不会是正确的。
为了使程序远离这类 缓冲区溢出buffer overread漏洞,如果尝试读取一个索引不存在的元素,Rust 会「停止执行并拒绝继续」。
backtrace 是一个执行到目前位置所有被调用的函数的列表。Rust 的 backtrace 跟其他语言中的一样:阅读 backtrace 的关键是从头开始读直到发现你编写的文件。这就是问题的发源地。这一行往上是你的代码所调用的代码;往下则是调用你的代码的代码。这些行可能包含核心 Rust 代码,标准库代码或用到的 crate 代码。让我们将 RUST_BACKTRACE 环境变量设置为「任何不是 0 的值来获取 backtrace 看看」。
大部分错误并没有严重到需要程序完全停止执行。有时,一个函数会因为一个容易理解并做出反应的原因失败。
例如,如果因为打开一个并不存在的文件而失败,此时我们可能想要创建这个文件,而不是终止进程。
❝可以使用
Result类型来处理潜在的错误,Result是一个「枚举类型」。 ❞
enum Result<T, E> {
Ok(T),
Err(E),
}
T 和 E 是「泛型类型参数」;
T 代表成功时返回的 Ok 成员中的数据的类型E 代表失败时返回的 Err 成员中的错误的类型因为 Result 有这些泛型类型参数,我们可以将 Result 类型和标准库中为其定义的函数用于很多不同的场景,这些情况中需要返回的成功值和失败值可能会各不相同。
调用一个返回 Result 的函数,因为它可能会失败。
use std::fs::File;
fn main() {
let f = File::open("hello.txt");
}
File::open 函数的返回值类型是 Result<T, E>。这里泛型参数 T 放入了成功值的类型 std::fs::File,它是一个文件句柄。E 被用在失败值上时 E 的类型是 std::io::Error。
这个返回值类型说明 File::open 调用「可能会成功并返回一个可以进行读写的文件句柄。这个函数也可能会失败:例如,文件可能并不存在,或者可能没有访问文件的权限」。
File::open 需要一个方式告诉我们是成功还是失败,并同时提供给我们文件句柄或错误信息。而这些信息正是 Result 枚举可以提供的。
当 File::open 成功的情况下,变量 f 的值将会是一个包含文件句柄的 Ok 实例。在失败的情况下,f 的值会是一个包含更多关于出现了何种错误信息的 Err 实例。
use std::fs::File;
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(error) => {
panic!("打开文件发生错误: {:?}", error)
},
};
}注意与 Option 枚举一样,Result 枚举和其成员也被导入到了 prelude 中,所以就不需要在 match 分支中的 Ok 和 Err 之前指定 Result::。
这里我们告诉 Rust 当结果是 Ok 时,返回 Ok 成员中的 file 值,然后将这个文件句柄赋值给变量 f。match 之后,我们可以利用这个文件句柄来进行读写。
match 的另一个分支处理从 File::open 得到 Err 值的情况。在这种情况下,我们选择调用 panic! 宏。如果当前目录没有一个叫做 hello.txt 的文件,用 panic! 宏的输出。
上面代码不管 File::open 是因为什么原因失败都会 panic!。我们真正希望的是对不同的错误原因采取不同的行为:
File::open 因为文件不存在而失败,我们希望创建这个文件并返回新文件的句柄。File::open 因为任何其他原因失败,例如没有打开文件的权限,使用 panic!处理。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!("创建文件失败: {:?}", e),
},
other_error => panic!("打开文件失败: {:?}", other_error),
},
};
}File::open 返回的 Err 成员中的值类型 io::Error,它是一个标准库中提供的「结构体」。这个结构体有一个返回 io::ErrorKind 值的 kind 方法可供调用。io::ErrorKind 是一个标准库提供的枚举,它的成员对应 io 操作可能导致的不同错误类型。我们感兴趣的成员是 ErrorKind::NotFound,它代表尝试打开的文件并不存在。这样,match 就匹配完 f 了,不过对于 error.kind() 还有一个内层 match。
我们希望在内层 match 中检查的条件是 error.kind() 的返回值是否为 ErrorKind 的 NotFound 成员。如果是,则尝试通过 File::create 创建文件。然而因为 File::create 也可能会失败,还需要增加一个内层 match 语句。当文件不能被打开,会打印出一个不同的错误信息。外层 match 的最后一个分支保持不变,这样对任何除了文件不存在的错误会使程序 panic。
Result<T, E> 类型定义了很多辅助方法来处理各种情况。其中之一叫做 unwrap。
Result 值是成员 Ok,unwrap 会返回 Ok 中的值。Result 是成员 Err,unwrap 会为我们调用 panic!。use std::fs::File;
fn main() {
let f = File::open("hello.txt").unwrap();
}
另一个类似于 unwrap 的方法它还允许我们选择 panic! 的错误信息:expect。使用 expect 而不是 unwrap 并提供一个好的错误信息可以表明你的意图并更易于追踪 panic 的根源。
use std::fs::File;
fn main() {
let f = File::open("hello.txt").expect("打开文件失败");
}expect 与 unwrap 的使用方式一样:返回文件句柄或调用 panic! 宏。expect 在调用 panic! 时使用的错误信息将是我们传递给 expect 的参数,而不像 unwrap 那样使用默认的 panic! 信息。
当编写一个会调用一些可能会失败的操作的函数时,除了在这个函数中处理错误外,还可以选择让调用者知道这个错误并决定该如何处理。这被称为 传播propagating错误,这样能更好的控制代码调用,因为比起你代码所拥有的上下文,调用者可能拥有更多信息或逻辑来决定应该如何处理错误。
下面展示了一个从文件中读取用户名的函数。如果文件不存在或不能读取,这个函数会将这些错误返回给调用它的代码:
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),
}
}Result<String, io::Error>。这意味着函数返回一个 Result<T, E> 类型的值,其中泛型参数 T 的具体类型是 String,而 E 的具体类型是 io::Error。
String 的 Ok 值 —— 函数从文件中读取到的用户名。Err 值,它储存了一个包含更多这个问题相关信息的 io::Error 实例。这里「选择 io::Error 作为函数的返回值是因为它正好是函数体中那两个可能会失败的操作的错误返回值」:File::open 函数和 read_to_string 方法。函数体以 File::open 函数开头。接着使用 match 处理返回值 Result,当 Err 时不再调用 panic!,而是提早返回并将 File::open 返回的「错误值作为函数的错误返回值传递给调用者」。如果 File::open 成功了,我们将文件句柄储存在变量 f 中并继续。
? 运算符Rust 提供了 ? 问号运算符来使错误传播更易于处理。
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)
}
Result 值之后的 ? 被定义为与处理 Result 值的 match 表达式有着完全相同的工作方式。
Result 的值是 Ok,这个表达式将会返回 Ok 中的值而程序将继续执行。Err,Err 中的值将作为整个函数的返回值,就好像使用了 return 关键字一样,这样错误值就被「传播给了调用者」。❝
? 运算符消除了大量样板代码并使得函数的实现更简单。我们甚至可以在?之后直接使用「链式方法调用」来进一步缩短代码。 ❞
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)
}我们对 File::open("hello.txt")? 的结果直接链式调用了 read_to_string,而不再创建变量 f。仍然需要 read_to_string 调用结尾的 ?,而且当 File::open 和 read_to_string 都成功没有失败时返回包含用户名 s 的 Ok 值。