0%

Ch 09.02:带有结果的可恢复错误

带有结果的可恢复错误

大多数错误并不严重到需要程序完全停止。有时,当一个函数失败时,其原因可以很容易地解释和响应。例如,如果您尝试打开一个文件,但由于该文件不存在而导致该操作失败,那么您可能希望创建该文件,而不是终止该进程。

回到第二章Result的枚举定义里,有两个成员,OkErr,如下:

1
2
3
4
enum Result<T, E> {
Ok(T),
Err(E),
}

TE是泛型参数:我们将在第十章详细讨论。目前你需要知道当成功时返回枚举成员Ok,当失败时返回枚举成员Err。由于Result具有这些泛型类型参数,因此我们可以在许多不同的情况下使用Result类型及其上定义的函数,其中我们希望返回的成功值和错误值可能不同。

我们来调用一个返回值类型为Result的函数,因为调用可能会失败。如Listing 9-3我们尝试打开一个文件。
Filename: src/main.rs

1
2
3
4
use std::fs::File;
fn main() {
let greeting_file_result = File::opend("hello.txt);
}

Listing 9-3 Openging a file

File::open的返回值是一个Result<T, E>。泛型参数T已经被File::open实现,并填入了成功的值,std::fs::File是一个文件句柄(file handle)。类型E的错误值是std::io::ErrorFile::open返回类型意思是如果成功就会返回一个文件句柄,并且可以进行读写。这个函数也有可能会调用失败:例如,文件不存在,或者没有权限访问。File::open函数需要有个方式告诉我们是成功或者失败,同时返回给我们文件句柄或者错误信息。这个信息正是Result枚举所传达的。

因此,当File::open调用成功,变量greeting_file_result的值将会是成员Ok并包含一个文件句柄。如果失败,greeting_file_result就是一个Err的实例并包含更多错误信息来展示到底发生了什么错误。

我们需要在Listing9-3中根据File::open的返回值来添加额外的代码。如Listing9-4中所示,这是一个基本的Result的处理工具,就是使用match表达式(我们已经在第六章中讲过了)。
Filename:src/main.rs

1
2
3
4
5
6
7
8
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),
};
}

Listing 9-4: Using a match expression to handle the Result variants that might be returned

请注意,与Option enum一样,Result枚举及其成员也通过prelude进入了作用域,因此我们不需要在匹配分支中的Ok和Err成员之前指定Result::

当结果为Ok时,这段代码将返回Ok中的file,然后我们将该文件句柄值赋给变量greeting_file。在match之后,我们可以使用文件句柄进行读写了。
match的另外一个分支就会从File::open中得到一个Err的值。在这个示例中,我们现在调用panic!宏(macro)。如果没有一个叫hello.txt的文件在当前文件夹,并且运行了这段代码,我们就会看到如下来自panic!宏的错误输出:

1
2
3
4
5
6
$ cargo run
Compiling error-handling v0.1.0 (file:///projects/error-handling)
Finished dev [unoptimized + debuginfo] target(s) in 0.73s
Running `target/debug/error-handling`
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

像往常一样,这个输出告诉我们哪里出了问题。

匹配不同的错误(Matching on Different Errors)

Listing9-4无论File::open因为什么失败都会报pannic!错误。然而,我们希望针对不同的失败原因采取不同的行动:如果File::open失败是因为文件不存在,我们想创建一个文件并返回新文件的句柄。如果File::open失败是因为其他原因–比如,因为我们没有打开文件的权限–我们仍然和Listing9-4一样panic!。为此,我们在match内添加一个内部表达式,如Listing 9-5所示。
Filename: src/main.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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);
}
},
}
}

Listing 9-5: Handling different kinds of errors in different ways

File::open内部返回的Err的值io::Error,它是标准库提供的数据结构。这个数据结构有一个kind方法可以得到一个io::ErrorKind的值。枚举io::ErrorKind是标准库提供的,并且有不同类型的错误都对应着相应的io操作。我们使用的ErrorKing::NotFound枚举成员表明我们尝试打开一个不存在的文件。所以我们在greeting_file_result上匹配,但我们也在error.kind()上进行内部匹配。

我们希望在内部匹配中检查的条件是error.kind()返回的值是否为ErrorKind枚举的NotFound成员。如果是我,我们将尝试通过File::create创建文件,然而我们创建文件也有可能失败,在match内部我们需要第二个分支来处理。当文件不能创建,一个不同的错误就会被打印。match外部保持不变。因此,除文件不存在的情况之外都会报错。

失败时 panic 的简写: unwrap 和 expect

match 已经很好用了,不过它可能有点冗长并且不总是能很好的表明其意图。Result<T, E> 类型定义了很多辅助方法来处理各种情况。其中之一叫做 unwrap,它的实现就类似于Listing 9-4 中的 match 语句。如果 Result 值是成员 Okunwrap 会返回 Ok 中的值。如果 Result 是成员 Errunwrap 会为我们调用 panic!。这里是一个实践 unwrap 的例子:

1
2
3
4
5
use std::fs:File;

fn main() {
let greeting_file = File::open("hello.txt").unwrap();
}

如果我们运行这段代码,并且hello.txt文件不存在,我们就会看到一个错误信息来自unwrap方法调用了panic!:

1
2
3
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方法也可以自定义panic!的错误信息。使用expect而不是unwrap并提供友好的错误信息可以传达您的意图,并使跟踪panic!的来源更容易。expect的语法如下所示:
Filename: src/main.rs

1
2
3
4
5
6
use std::fs:File;

fn main() {
let greeting_file = File::open("hello.txt")
.expect("hello.txt should be included int this project");
}

我们可以和使用unwrap一样使用expect来返回一个文件句柄或者调用panic!。错误信息通过expect调用panic!时传递,而不是panic!默认的错误信息,展示如下:

1
2
3
thread 'main' panicked at 'hello.txt should be included in this project: Os {
code: 2, kind: NotFound, message: "No such file or directory" }',
src/main.rs:5:10

传递错误(Propagating Errors)

当编写一个其实先会调用一些可能会失败的操作的函数时,除了在这个函数中处理错误外,还可以选择让调用者知道这个错误并决定该如何处理。这被称为 传播(propagating)错误,这样能更好的控制代码调用,因为比起你代码所拥有的上下文,调用者可能拥有更多信息或逻辑来决定应该如何处理错误。
例如:Listing 9-6所示,一个函数读一个文件。如果文件不存在或者不能读,函数就会返回一些错误。
Filename: src/main.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
let usename_file_result = File::open("hello.txt");

let mut usename_file = match username_file_result {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut username = String::new();

match user_file.read_to_string(&mut username) {
Ok(_) => Ok(username),
Err(e) => Err(e),
}
}

fn main() {
read_username_from_file();
}

Listing 9-6: A function that returns errors to the calling code using match

这个函数可以用更短的方式编写,但我们将从手动做很多事情开始,以探索错误处理;最后,我们将展示较短的方法。我们先看一下函数的返回类型: Result<String, io::Error> 。这意味着该函数返回一个类型的 Result<T, E> 值,其中泛型参数 T 已用具体类型填充,泛型类型已用具体类型 String填充,Eio::Error填充。

如果此函数成功且没有任何问题,则调用此函数的代码将收到一个Ok值,该值包含String此函数从文件中读取的username。如果此函数遇到任何问题,调用代码将收到一个 Err 值,该值包含包含有关问题所在的详细信息的实例 io::Error 。我们选择 io::Error 此函数的返回类型,因为这恰好是我们在此函数主体中调用的两个操作返回的错误值的类型: File::open 函数和 read_to_string 方法。

函数的主体从调用 File::open 函数开始。然后我们用类似于Listing 9-4 match 中的值来处理 matchResult 值。如果成功,模式 File::open 变量中的文件句柄将成为可变变量 username_file 中的值,函数将继续。在这种情况下 Err ,我们不是调用,而是使用 return 关键字提前完全返回函数,并从File::open返回的错误传递出去,模式匹配中的e作为该函数的错误值传递回调用 panic! 代码。

因此,如果我们在username_file中有一个文件句柄,那么该函数将在变量username中创建一个新的String,并调用username_file中文件句柄上的read_to_string方法来将文件的内容读入usernameread_to_string方法也返回Result,因为它可能失败,即使File::open打开文件成功,读文件也有可能失败。所以我们需要另一个匹配来处理这个Result:如果read_to_string成功,那么我们的函数就成功了,我们从文件中返回username,这个username现在被Ok封装在username中。如果read_to_string操作失败,返回错误值的方式与处理File::open返回值的匹配中返回错误值的方式相同。然而,我们不需要显式return,因为这是函数中的最后一个表达式。

然后,调用该代码的代码将处理获取包含usernameOk值或包含io::ErrorErr值。由调用代码决定如何处理这些值。如果调用代码得到一个Err值,它可能会调用panic!并使程序崩溃,使用默认username,或者从文件以外的其他地方查找username。我们没有足够的信息来了解调用代码实际尝试做什么,所以我们向上传播所有成功或错误信息,以便它正确处理。

这种传播错误的模式在Rust中非常常见,因此Rust为了方便起见提供了问号操作符?

传播错误的快捷方式:?操作符

如Listing9-7所示,实现了一个和Listing9-6相同的read_username_from_file的函数,但是使用了?操作来实现。
Filename: src/main.rs

1
2
3
4
5
6
7
8
9
10
11
12
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() {
}

Listing 9-7: A function that returns errors to the calling code using the `?`` operator

?操作符放置在Result值之后,其工作方式与Listing 9-6中为处理Result值而定义的匹配表达式几乎相同。如果Result的值为Ok,则该表达式将返回Ok中的值,程序将继续执行。如果该值为Err,则整个函数将返回Err,就像我们使用了return关键字一样,因此错误值将传播到调用代码。

Listing 9-6中的匹配表达式的作用与?操作符所做的事情:错误值有?在标准库中的Fromtrait中定义了from函数,该函数用于将值从一种类型转换为另一种类型。什么时候?操作符调用from函数,接收到的错误类型被转换为当前函数返回类型中定义的错误类型。当函数返回一种错误类型来表示函数可能失败的所有方式时,即使部分可能因许多不同的方式而失败,这也是有用的。

例如,我们可以修改Listing 9-7中的read_username_from_file函数,使其返回一个自定义的错误类型OurError。如果我们还为OurError定义impl From<io::Error>,从io::Error构造OurError的实例,那么?操作符会在read_username_from_file函数体中将调用from并转换错误类型,而无需向函数中添加任何代码。

在Listing 9-7的上下文中,?File::open调用的末尾将把Ok中的值返回给变量username_file。如果发生错误,?操作符将提前返回,并向调用代码提供任何Err值。同样的道理也适用于?read_to_string调用结束时。

?操作符消除了大量的模板代码,使这个函数的实现更简单。我们甚至可以通过在?之后立即连接方法调用来进一步缩短代码,如Listing 9-8所示。
Filename: src/main.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
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() {
}

Listing 9-8: Chaining method calls after the ? operator

我们将usernameString的创建移到了函数的开头;这一点没有改变。我们没有创建一个可变的用户名文件,而是将read_to_string调用直接连接到file::open("hello.txt")的结果上。我们还有?File::openread_to_string都成功时,我们仍然返回一个包含usernameOk值,而不是返回错误。功能与Listing 9-6和Listing 9-7相同;这是一种不同的,更符合工程学的写法。

Listing 9-9 所示使用了fs::read_to_string将使代码更加简短。

1
2
3
4
5
6
7
8
use std::fs;
use std::io;

fn read_username_from_file() -> Result<String, io::Error> {
fs::read_to_string("hello.txt")
}
fn main() {
}

Listing 9-9: Using fs::read_to_string instead of opening and then reading the file

将文件读入字符串是一种相当常见的操作,因此标准库提供了方便的fs::read_to_string函数,该函数打开文件,创建一个新的String,读取文件的内容,将内容放入该String,并返回它。当然,使用fs::read_to_string并不能让我们有机会解释所有的错误处理,所以我们先用更长的方法来解释。

哪里可以使用?操作

?操作符只能用于返回类型与?操作符兼容的函数中。这是因为?操作符的定义是执行从函数中提前返回一个值,方式与Listing 9-6中定义的match表达式相同。在Listing 9-6中,匹配使用一个Result值,而提前返回臂返回一个Err(e)值。函数的返回类型必须是Result,以便与此返回兼容。

在Listing 9-10中,让我们看看如果使用?返回类型与我们使用的值的类型不兼容的主函数中的操作符?:
Filename: src/main.rs

1
2
3
4
5
use std::fs::File;

fn main() {
let greeting_file = File::open("hello.txt")?;
}

Listing 9-10: Attempting to use the ? in the main function that returns `()`` won’t compile

这段代码打开一个文件,可能会失败。?操作符在File::open返回的Result值之后,但是这个主函数的返回类型是(),而不是Result。当我们编译这段代码时,会得到以下错误消息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ cargo run
Compiling error-handling v0.1.0 (file:///projects/error-handling)
error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`)
--> src/main.rs:4:48
|
3 | fn main() {
| --------- this function should return `Result` or `Option` to accept `?`
4 | let greeting_file = File::open("hello.txt")?;
| ^ cannot use the `?` operator in a function that returns `()`
|
= help: the trait `FromResidual<Result<Infallible, std::io::Error>>` is not implemented for `()`

For more information about this error, try `rustc --explain E0277`.
error: could not compile `error-handling` due to previous error

这个错误指出我们只允许使用?返回ResultOption或其他实现FromResidual的类型的函数中的操作符。

要修复这个错误,您有两种选择。一种选择是更改函数的返回类型,使其与使用的值兼容。只要没有限制,就继续操作。另一种技术是使用matchResult<T, E>方法之一,以任何合适的方式处理Result<T, E>

错误信息中还提到?也可以与Option<T>值一起使用。就像使用?Result中,您只能使用?在返回一个Option的函数中使用Option?操作符在Option<T>上调用时的行为与在Result<T, E>上调用时的行为相似:如果值为None,则在该点将提前从函数返回None。如果值是Some,则Some中的值是表达式的结果值,函数继续执行。Listing 9-11给出了一个函数示例,该函数查找给定格式中第一行的最后一个字符:

1
2
3
4
5
6
7
8
9
10
11
12
13
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);
}

Listing 9-11: Using the ? operator on an Option<T> value

这个函数返回Option<char>,因为有可能有字符,但也有可能没有。这段代码接受text字符串切片参数并对其调用lines方法,该方法返回一个遍历字符串中的行的迭代器。因为这个函数想要检查第一行,所以它在迭代器上调用next以从迭代器中获取第一个值。如果text是空字符串,对next的调用将返回None,在这种情况下我们使用?停止并从第一行的最后一个字符返回None。如果text不是空字符串,next将返回一个Some值, 其中包含text中第一行的字符串切片。

?操作符提取字符串切片,然后调用该字符串切片上的chars来获取其字符的迭代器。我们对第一行的最后一个字符感兴趣,因此调用last来返回迭代器中的最后一项。这是一个选项,因为第一行可能是空字符串,例如,如果文本以空行开头,但在其他行上有字符,如“\nhi”。但是,如果第一行有最后一个字符,它将在Some变体中返回。?运算符在中间给了我们一种简洁的方式来表达这个逻辑,允许我们实现

注意,您可以使用?操作符对返回Result的函数中的Result进行操作,您可以使用?操作符在返回Option的函数中对Option进行操作,但不能混合匹配。?操作符不会自动将Result转换为Option,反之亦然;在这些情况下,您可以使用诸如Result上的ok方法或Option上的ok_or方法来显式地进行转换。

到目前为止,我们使用的所有主要函数都是return()main函数的特殊之处在于它是可执行程序的入口和出口点,它的返回类型是有限制的,这样程序才能按照预期的方式运行。

幸运的是,main也可以返回Result<(), E>。Listing 9-12拥有Listing 9-10的代码,但我们将main的返回类型更改为Result<()Box<dyn Error>>,并在末尾添加返回值Ok(())。这段代码现在可以编译了:

1
2
3
4
5
6
7
8
use std::error::Error;
use std::fs::File;

fn main() -> Result<(), Box<dyn Error>> {
let greeting_file = File::open("hello.txt")?;

Ok(())
}

Listing 9-12: Changing main to return Result<(), E> allows the use of the ? operator on Result values

Box<dyn Error>类型是一个trait对象,我们将在第17章使用允许不同类型值的trait对象一节中讨论它。现在,您可以读取Box<dyn Error>来表示任何类型的错误。使用?允许在错误类型为Box<dyn error >的主函数中返回Result值,因为它允许提前返回任何Err值。即使这个主函数的主体只会返回std::io::Error类型的错误,通过指定Box<dyn Error>,即使将返回其他错误的更多代码添加到main的主体中,该签名仍然是正确的。

main函数返回Result<(), E>时,如果main函数返回Ok(()),可执行程序将以0的值退出;如果main函数返回Err值,可执行程序将以非0的值退出。用C编写的可执行程序在退出时返回整数:成功退出的程序返回整数0,出错的程序返回非0的整数。Rust还从可执行文件返回整数,以与此约定兼容。

main函数可以返回任何实现std::process::Termination trait的类型,它包含一个可以返回ExitCode的函数report。有关为您自己的类型实现Termination特性的更多信息,请参阅标准库文档。

到现在,我们已经详细讨论了调用panic!或者返回Result

宇宙山河浪漫,赞赏动力无限

Welcome to my other publishing channels