Rust学习笔记之错误处理
作出决策就是要求我们在一个目标与另一个目标直接进行权衡取舍
大家好,我是柒八九。
今天,我们继续Rust学习笔记的探索。我们来谈谈关于错误处理的相关知识点。
如果,想了解该系列的文章,可以参考我们已经发布的文章。如下是往期文章。
文章list
- Rust学习笔记之Rust环境配置和入门指南
- Rust学习笔记之基础概念
- Rust学习笔记之所有权
- Rust学习笔记之结构体
- Rust学习笔记之枚举和匹配模式
- Rust学习笔记之包、Crate和模块
- Rust学习笔记之集合
你能所学到的知识点
- panic! 与不可恢复的错误 推荐阅读指数 ⭐️⭐️⭐️⭐️
- Result 与可恢复的错误 推荐阅读指数 ⭐️⭐️⭐️⭐️
好了,天不早了,干点正事哇。
在很多情况下,Rust
要求你承认出错的可能性,并在编译代码之前就采取行动。这些要求使得程序更为健壮,它们确保了你会在将代码部署到生产环境之前就发现错误并正确地处理它们!
Rust
将错误组合成两个主要类别:{可恢复错误|recoverable}和 {不可恢复错误|unrecoverable}。
- 可恢复错误通常代表向用户报告错误和重试操作是合理的情况,比如未找到文件
- 不可恢复错误通常是
bug
的同义词,比如尝试访问超过数组结尾的位置。
panic! 与不可恢复的错误
有的时候代码出问题了,而你对此束手无策。对于这种情况,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
文件的第二行第五个字符。
使用 panic! 的 backtrace
来看看另一个因为我们代码中的 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
类型来处理潜在的错误,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
。
失败时 panic 的简写:unwrap 和 expect
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 值
。
后记
分享是一种态度。
参考资料:《Rust权威指南》
全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。
转载自:https://juejin.cn/post/7212576051166347322