前言#
我们昨天学了panic以及如何查看详细的报错信息。
今天我们继续往下学Result
可恢复的(recoverable)错误和Result#
昨天提到过了,有些场景是不需要panic的,比如读取某个文件但是这个文件不存在,这个时候就没必要去崩溃,给个提示,或者给它生成一个新文件。
我们之前应该是用到过Result这个枚举的,你应该还记得是一个枚举,有Ok和Err这两个变体(varant),它们也是默认就prelude的,所以不需要我们手动去引入。
enum Result<T, E> {
Ok(T),
Err(E),
} T自然是调用成功的返回值的类型,而E则是错误场景的类型。
我们来写个例子,打开a.txt文件
use std::fs::File;
fn main() {
let greeting_file_result = File::open("hello.txt");
} open这个关联函数返回的是一个io::Result的枚举

你会发现并没有报错,但是我们确实没这个文件,为什么呢?因为这段代码是runtime调用时才会知道结果。我们来运行下

可以看到返回的错误数据是一个Os的struct也就是构造体。
如果处理这种场景相信大家都很熟悉了,没错就是match,我们来改下我们的例子
use std::fs::File;
fn main() {
let greeting_file_result = File::open("hello.txt");
let file = match greeting_file_result {
Ok(file) => {
dbg!(file);
},
Err(e) => {
dbg!(e);
}
};
} 这个时候就不会报错啦
但是我们还可以优化下,当找不到文件的时候给它创建一个文件,这会使用到File::create这个关联函数。
匹配不同的错误#
在我们使用File::create之前,我们需要知道是什么错误类型才行,因为有可能是没有读的权限导致打开失败,这种时候是不能继续往下执行的,应该panic!掉。
这就需要用到Error的一个method--kind,会返回错误类型,来看下代码
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);
}
},
};
}可以看到我们使用了一个io里的ErrorKind这一个枚举,简单的看下部分数据

可以看到第一个就是NotFound
然后我们调用了error里的kind这个方法,这个方法返回了ErrorKind中的变体。
我们只需要NotFound这一个变体,所以其他场景就忽略了。
然后调用File::create这一关联函数创建文件,当然,创建文件也是有可能会出问题的,所以这里又来了个match处理错误场景。
最后cargo run一下

确实生成了。
现在我们的代码无懈可击,但是也是乱的一批,三层match套娃会让人觉得恶心,所以rust针对这种场景提供了类似的method,我们来看下。
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let greeting_file = File::open("hello.txt").unwrap_or_else(|error| {
if error.kind() == ErrorKind::NotFound {
File::create("hello.txt").unwrap_or_else(|error| {
panic!("Problem creating the file: {:?}", error);
})
} else {
panic!("Problem opening the file: {:?}", error);
}
});
}
这里面用到了闭包(closures)这一概念,我们后面会学到。两块代码实际效果是一样的,但是利用闭包的这种会更优雅些。
这里还用到了unwrap_or_else的method,这个方法主要功能是ok直接返回value,而error场景则执行传入的闭包。

可以看到参数接收的是一个callback,这个callback接收e这个数据作为参数,最后要求这个callback返回的类型和Ok的是一样的。
这么看,貌似和js中的闭包效果差不多?这个问题留到学闭包的时候再说,先mark起来。
一些简写:unwrap以及expect#
我们直接来看下例子,先来看unwrap的
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt").unwrap();
}
来看下unwrap的源码


这玩意儿一目了然,出错直接给你panic,没问题返回value。
然后我们再来看下expect的例子
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt")
.expect("hello.txt should be included in this project");
} 其实我们之前已经用过了,看下源码

没啥好说的,相对于unwrap来说多了个自定义错误提示语的功能。
所以常规情况下用expect即可。
错误传递(propagating)#
刚才我们的例子都是直接在match的block中直接处理的Err场景,但有时候把错误抛出来会更有用,我们来看下例子
#![allow(unused)]
fn main() {
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),
}
}
}
可以看到我们return(我之前好像说rust里没有return?如果说了当我没说)了一个Err(e),这个Err(e)中的e的类型是io:Error,然后我们看到调用了read_to_string这个方法,将数据写入username这个变量中, 但是read_to_string这个方法可能也会失败,所以这里又需要match一次。
上面说过它们(Result、Ok、Err)都是prelude的,所以这里直接调用Err是没问题的。
注意最后一个match语句并没有;,也就是说这个结果将作为函数的返回值。
还需要注意的一点是最后这个函数是Ok(_) => Ok(username)以及Err(e) => Err(e)
那么函数将有两种可能,Ok或者Err 这两种带值的变体,那么自然类型就会是一个Result,而T则是username,e为io:Error。
这样我们就结果交由开发者自己了。
但是上面这样的操作老实说还是有些麻烦,rust官方也提供了简写的方式。
使用?来简写错误传递#
我们直接来看下例子
#![allow(unused)]
fn main() {
use std::fs::File;
use std::io;
use std::io::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)
}
}?操作符跟在了open方法后面,这样写相当于是unwrap方法,但是错误时返回Err(e),和刚才的代码也是有差别的,刚才我们的代码成功的时候返回的是Ok(username),而使用?操作符并不会返回Ok(username),而是直接返回value。这也就是为什么最后需要手动Ok(username)。
可以看到代码明显少了很多,结构也更扁平了。然而上面的代码其实还是可以再优化的,因为username_file这个临时变量可以不用写,直接链式调用。
#![allow(unused)]
fn main() {
use std::fs::File;
use std::io;
use std::io::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)
}
}
这样就优雅啦~
但是,其实还有更更优雅的
#![allow(unused)]
fn main() {
use std::fs;
use std::io;
fn read_username_from_file() -> Result<String, io::Error> {
fs::read_to_string("hello.txt")
}
}我们直接调用的fs模块的read_to_string关联函数,它返回的类型正好就是Result
是不是非常优雅?当然,如果你不想要io:Error这个错误类型,那只能是自己写了,这玩意儿只适用于这场景。
而?是所有时候都能用的吗?并不是
哪些可以用?操作符#
?操作符仅支持返回类型为Result/Option以及实现了FromResidual的类型这三种类型。
来看个例子

我们是直接在main函数里使用的,而main函数默认是()空元组,那要怎么办呢?
其实main函数也是可以改变返回类型的,我们来看下例子
use std::fs::File;
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
let greeting_file = File::open("hello.txt")?;
Ok(())
}表示允许接收任何错误类型,这个暂时不说啥原理,以后会学到。
这种错误传递的场景同样适用于Option,和Result一样,函数的返回值也一定得是Option,我们来看个例子
fn last_char_of_first_line(text: &str) -> Option<char> {
text.lines().next()?.chars().last()
} 其中lines表示读取行,遇到\n就算一行,next表示迭代,和es6+的iterator差不多。
简单地说这个last_char_of_first_line方法是获取每一行的最后一个字符,返回的是Some(char)或者None这俩变体其中一个,所以得是Option的类型。毕竟行的时候可能读不到。
总的来说,这个?和es6+提供的?也是蛮像的,至少错误场景都不会直接报错崩溃,有个空值保护的空间。
总结#
这个量有点多老实说,没想到可恢复错误场景这块内容这么多。。。。
重点是这个?操作符和错误传播这两个点,其他点都比较好理解。
最后,如果觉得这篇文章对你有帮助,请务必点个赞,谢谢~
参考#
- ^rust-recoverable-error-and-result https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html#recoverable-errors-with-result
- ^rust-matching-on-different-errors https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html#matching-on-different-errors
- ^rust-shortcut-use-unwrap-or-expect https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html#shortcuts-for-panic-on-error-unwrap-and-expect
- ^rust-propagating-errors https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html#propagating-errors
- ^rust-shortcut-for-prepagating-errors-with-?-operator https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html#a-shortcut-for-propagating-errors-the--operator
- ^rust-when-can-use-?-operator https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html#where-the--operator-can-be-used
编辑于 2022-12-13 12:23・IP 属地广东
