一. 简述
在Rust
中,我们将错误分为两大类:可恢复错误和不可恢复错误。
-
可恢复错误:例如文件未找到等,一般需要它们报告给用户并再次尝试进行操作;
-
不可恢复错误:这类错误往往就是
Bug
的另一种说法,比如尝试访问超出数组结尾的位置等;
在Rust
种没有类似的异常机制,但它提供了用于可恢复错误的类型Result<T, E>
,以及在程序出现不可恢复错误时中止运行的panic!
宏。
二. 不可恢复错误
在Rust
种提供了一个特殊的panic!
宏。程序会在panic!
宏执行时打印出一段错误提示信息,展开并清理当前的调用栈,然后退出程序。
fn main() {
panic!("crash and burn");
}
此时运行时,会看到如下所示输出:
Finished dev [unoptimized + debuginfo] target(s) in 0.10s
Running `target\debug\rust-qwzn.exe`
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
发生时,程序会默认开始展开。这意味着Rust
会沿着调用栈的反向顺序遍历所有调用函数,并依次清理这些函数中的数据。加入项目需要最终二进制包尽可能的小,那么你可以通过在Cargo.toml
文件中的[profile.release]
区域添加panic = 'abort'
将panic
的默认行为从展开切换为终止。
在上面我们可以看到我们可以通过设置环境变量RUST_BACKTACE
输出回溯信息。这个和其他的编程语言差不多,如下:
yuelong@yuelongdeMBP rust-example % RUST_BACKTRACE=1 cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.01s
Running `target/debug/rust-example`
thread 'main' panicked at 'hello', src/main.rs:3:5
stack backtrace:
0: rust_begin_unwind
at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/std/src/panicking.rs:584:5
1: core::panicking::panic_fmt
at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/core/src/panicking.rs:142:14
2: rust_example::main
at ./src/main.rs:3:5
3: core::ops::function::FnOnce::call_once
at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/core/src/ops/function.rs:248:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
三. 可恢复错误
在日常开发中,大部分错误其实都没有严重到需要整个程序停止运行的地步。函数常常会由于一些可以简单解释并做出响应的原因而运行失败。
3.1. Result枚举
此时我们介绍一个枚举Result
,在其中定义了两个变体:Ok
和Err
,如下:
pub enum Result<T, E> {
Ok(T),
Err(E),
}
下面举一个使用Result
的场景,文件的打开:
let result = File::open("hello.txt"); // result类型时Result<File>
此时需要注意这个Result
是io::Result
,它是通过类型别名重新定义的:
pub fn open<P: AsRef<Path>>(path: P) -> io::Result<File> {
OpenOptions::new().read(true).open(path.as_ref())
}
// 类型别名
pub type Result<T> = result::Result<T, Error>;
此时说明File::Open
调用可能成功,并返回读写文件的句柄;但是调用通常可能会失败,例如文件不存在。这时File::Open
函数就可以通过Result
枚举通知用户是否调用成功。
let f = match File::open("hello.txt") {
Ok(file) => file,
Err(error) => panic!("打开文件发生错误:{}", error),
};
这里如果发生错误都会直接panic
比较粗糙,这里我们还可以在使用一个match
匹配多种错误:
match File::open("hello.txt") {
Ok(file) => file,
// 打开发生错误,匹配具体错误
Err(error) => match error.kind() {
// 文件不存在错误,则创建文件
ErrorKind::NotFound => match File::create("hello.txt") {
// 创建成功,则返回
Ok(fc) => fc,
Err(e) => panic!("尝试创建hello.txt文件失败:{:?}", e)
},
// 其他错误不做处理,直接panic
other => panic!("打开文件发生错误:{:?}", other),
},
};
上面的多个match
有点套娃的意思,下面我们使用闭包优化下代码,如果文件打开失败,判断是否是文件不存在的错误,如果是则创建文件:
File::open("hello.txt").map_err(|error| { // 闭包map_err可用来处理错误,并传递成功结果
if error.kind() == ErrorKind::NotFound {
// 闭包unwrap_or_else,可以用来返回成功的结果,或者发生错误后从闭包中结算得到结果返回
File::create("hello.txt").unwrap_or_else(|error| { panic!("尝试创建hello.txt文件失败:{:?}", error); });
} else {
panic!("打开文件发生错误:{:?}", error);
}
});
注意:如果需要查看
map_err
和unwrap_or_else
的具体使用可以查看标准库
有时候我们不需要这么冗长的错误处理,下面我们介绍下更加快捷的处理方式:unwrap
和expect
:
-
unwrap
:实现match
表达式的效果;当Result
的返回值是Ok
变体时,unwrap
就会返回Ok
内部的值;而当Result
的返回值是Err
变体时,unwrap
则会替我们调用panic!
宏。可以看源码实现:pub fn unwrap(self) -> T where E: fmt::Debug { match self { Ok(t) => t, Err(e) => unwrap_failed("called `Result::unwrap()` on an `Err` value", &e), } } // 输出panic fn unwrap_failed(msg: &str, error: &dyn fmt::Debug) -> ! { panic!("{msg}: {error:?}") }
针对上面的例子,我们使用
unwrap
可以出现如下效果:// 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:5:40 let file = File::open("hello.txt").unwrap();
-
expect
:它允许我们在unwrap
基础上,指定panic!
宏所附带的错误信息,这样可以方便我们追踪错误。// thread 'main' panicked at '文件不存在: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:5:40 let file = File::open("hello.txt").expect("文件不存在");
3.2. 返回Result
当我们编写的函数中包含一些可能会执行失败的调用时,除了可以在函数中处理这个错误,还可以将这个错误返回给调用者,它们决定应该如何做进一步的处理。这个过程也叫做传播错误。下面看一个例子:
fn read_file() -> Result<String, io::Error> {
// 打开文件
match File::open("hello.txt") {
// 打开成功
Ok(mut file) => {
// 创建一个可变字符串
let mut buf = String::new();
// 将文件数据读入buf中,并返回
match file.read_to_string(&mut buf) {
Ok(_) => Ok(buf),
// 错误则返回Err
Err(e) => Err(e)
}
},
Err(e) => Err(e)
}
}
虽然我们通过上面的代码实现了返回Result
的目的,但是还是有点冗长;恰恰Rust
又为我们提供了一个快捷方式:?
。下面我们看看使用?
如何简化我们上面的代码:
fn read_file_plus() -> Result<String, io::Error> {
let mut file = File::open("hello.txt")?;
let mut buf = String::new();
file.read_to_string(&mut buf)?;
Ok(buf)
}
正如我们看到的,通过放置的代码末尾的?
实现了我们上面的match
过程功能。假如这个Result
的值是Ok
,那么包含在Ok
中的值就会作为这个表达式的结果返回并继续执行程序。假如值是Err
,那么这个值就会作为整个程序的结果返回,这样就和使用return
一样的将错误传播给了调用者。
其实上面的代码还是有点多,我们可以在?
后面进行链式调用:
fn read_file_plus() -> Result<String, io::Error> {
let mut buf = String::new();
File::open("hello.txt")?.read_to_string(&mut buf)?;
Ok(buf)
}
是不是非常的nice!
3.3. main函数的Result
我们知道main
函数是一个特殊的函数,我们一般写main
函数是没有写返回值的(main
函数默认的返回值类型是()
),那么main
是否可以使用Result
呢?答案是肯定的:
fn main() -> Result<(), Box<dyn Error>> {
read_file_plus()?;
Ok(())
}
这里又出现了Box<dyn Error>
,我们很纳闷!不用着急后面章节会详细介绍。现在我们只需要知道Box<dyn Error>
是trait
对象(可以看作是接口),表示任何可能的错误类型。在拥有这种返回类型的main函数中使用?
运算符是合法的。
四. Result常用方法
下面我们列举一些Result的常用方法:
方法 | 描述 |
---|---|
unwrap | 返回包含 self 值的包含的 Ok 值。 |
expect | 返回包含 self 值的包含的 Ok 值。 |
and_then | 如果结果为 Ok ,则调用 op ,否则返回 self 的 Err 值。该函数可用于基于 Result 值的控制流。 |
map | 通过对包含的 Ok 值应用函数,将 Err 值 Maps 转换为 Result<U, E> ,而保持 Err 值不变。该函数可用于组合两个函数的结果。 |
ok | 从 Result<T, E> 转换为 Option<T> 。将 self 转换为 Option<T> ,使用 self ,并丢弃错误 (如果有) |
or | 如果结果为 Err ,则返回 res ; 否则,返回 self 的 Ok 值 |
err | 从 Result<T, E> 转换为 Option<E> 。将 self 转换为 Option<E> ,使用 self ,并丢弃成功值 (如果有) |
as_ref | 从 &Result<T, E> 转换为 Result<&T, &E> 。产生一个新的 Result ,其中包含对原始引用的引用,并将原始保留在原处。 |
更多的方法可以到标准库文档中查看:https://rustwiki.org/zh-CN/std/result/enum.Result.html
这里我们再将文件读取的例子在重写下,尽量使用更多的Result
中的方法:
use std::error::Error;
use std::fs::File;
use std::io::{Read};
fn read_file_plus(name: &str) -> Result<i32, String> {
// 打开文件
File::open(name)
// 重新定义err的内容
.map_err(|err| err.to_string())
// 使用and_then处理文件内容
.and_then(|mut file| {
let mut buf = String::new();
// 读取文件内容
file.read_to_string(&mut buf)
// 重新定义err的内容
.map_err(|err| err.to_string())
// 返回buf内容
.map(|_| buf)
}).and_then(|content| { // 处理内容
content.trim().parse::<i32>() // 先去掉空格
.map_err(|err| err.to_string()) // 定义错误
}).map(|n| 2 * n) // 将转换为i32的数字乘2
}
fn main() -> Result<(), Box<dyn Error>> {
let i = read_file_plus("hello.txt")?;
println!("{}", i); // 4
Ok(())
}
Result和Option这两个枚举有很多相似的方法,可以比较学习下。