## 用 `Result` 处理可恢复的错误 > [ch09-02-recoverable-errors-with-result.md](https://github.com/rust-lang/book/blob/main/src/ch09-02-recoverable-errors-with-result.md) >
> commit 699adc6f5cb76f6e9d567ff0a57d8a844ac07a88 大部分错误并没有严重到需要程序完全停止执行。有时候,一个函数失败,仅仅就是因为一个容易理解和响应的原因。例如,如果因为打开一个并不存在的文件而失败,此时我们可能想要创建这个文件,而不是终止进程。 回忆一下第二章 [“使用 `Result` 类型来处理潜在的错误”][handle_failure] 部分中的那个 `Result` 枚举,它定义有如下两个成员,`Ok` 和 `Err`: ```rust enum Result { Ok(T), Err(E), } ``` `T` 和 `E` 是泛型类型参数;第十章会详细介绍泛型。现在你需要知道的就是 `T` 代表成功时返回的 `Ok` 成员中的数据的类型,而 `E` 代表失败时返回的 `Err` 成员中的错误的类型。因为 `Result` 有这些泛型类型参数,我们可以将 `Result` 类型和标准库中为其定义的函数用于很多不同的场景,这些情况中需要返回的成功值和失败值可能会各不相同。 让我们调用一个返回 `Result` 的函数,因为它可能会失败:如示例 9-3 所示打开一个文件: 文件名:src/main.rs ```rust {{#rustdoc_include ../listings/ch09-error-handling/listing-09-03/src/main.rs}} ``` 示例 9-3:打开文件 `File::open` 的返回值是 `Result`。泛型参数 `T` 会被 `File::open` 的实现放入成功返回值的类型 `std::fs::File`,这是一个文件句柄。错误返回值使用的 `E` 的类型是 `std::io::Error`。这些返回类型意味着 `File::open` 调用可能成功并返回一个可以读写的文件句柄。这个函数调用也可能会失败:例如,也许文件不存在,或者可能没有权限访问这个文件。`File::open` 函数需要一个方法在告诉我们成功与否的同时返回文件句柄或者错误信息。这些信息正好是 `Result` 枚举所代表的。 当 `File::open` 成功时,`greeting_file_result` 变量将会是一个包含文件句柄的 `Ok` 实例。当失败时,`greeting_file_result` 变量将会是一个包含了更多关于发生了何种错误的信息的 `Err` 实例。 我们需要在示例 9-3 的代码中增加根据 `File::open` 返回值进行不同处理的逻辑。示例 9-4 展示了一个使用基本工具处理 `Result` 的例子:第六章学习过的 `match` 表达式。 文件名:src/main.rs ```rust,should_panic {{#rustdoc_include ../listings/ch09-error-handling/listing-09-04/src/main.rs}} ``` 示例 9-4:使用 `match` 表达式处理可能会返回的 `Result` 成员 注意与 `Option` 枚举一样,`Result` 枚举和其成员也被导入到了 prelude 中,所以就不需要在 `match` 分支中的 `Ok` 和 `Err` 之前指定 `Result::`。 这里我们告诉 Rust 当结果是 `Ok` 时,返回 `Ok` 成员中的 `file` 值,然后将这个文件句柄赋值给变量 `greeting_file`。`match` 之后,我们可以利用这个文件句柄来进行读写。 `match` 的另一个分支处理从 `File::open` 得到 `Err` 值的情况。在这种情况下,我们选择调用 `panic!` 宏。如果当前目录没有一个叫做 *hello.txt* 的文件,当运行这段代码时会看到如下来自 `panic!` 宏的输出: ```console {{#include ../listings/ch09-error-handling/listing-09-04/output.txt}} ``` 一如既往,此输出准确地告诉了我们到底出了什么错。 ### 匹配不同的错误 示例 9-4 中的代码不管 `File::open` 是因为什么原因失败都会 `panic!`。我们真正希望的是对不同的错误原因采取不同的行为:如果 `File::open `因为文件不存在而失败,我们希望创建这个文件并返回新文件的句柄。如果 `File::open` 因为任何其他原因失败,例如没有打开文件的权限,我们仍然希望像示例 9-4 那样 `panic!`。让我们看看示例 9-5,其中 `match` 增加了另一个分支: 文件名:src/main.rs ```rust,ignore {{#rustdoc_include ../listings/ch09-error-handling/listing-09-05/src/main.rs}} ``` 示例 9-5:使用不同的方式处理不同类型的错误 `File::open` 返回的 `Err` 成员中的值类型 `io::Error`,它是一个标准库中提供的结构体。这个结构体有一个返回 `io::ErrorKind` 值的 `kind` 方法可供调用。`io::ErrorKind` 是一个标准库提供的枚举,它的成员对应 `io` 操作可能导致的不同错误类型。我们感兴趣的成员是 `ErrorKind::NotFound`,它代表尝试打开的文件并不存在。这样,`match` 就匹配完 `greeting_file_result` 了,不过对于 `error.kind()` 还有一个内层 `match`。 我们希望在内层 `match` 中检查的条件是 `error.kind()` 的返回值是否为 `ErrorKind`的 `NotFound` 成员。如果是,则尝试通过 `File::create` 创建文件。然而因为 `File::create` 也可能会失败,还需要增加一个内层 `match` 语句。当文件不能被创建,会打印出一个不同的错误信息。外层 `match` 的最后一个分支保持不变,这样对任何除了文件不存在的错误会使程序 panic。 > 不同于使用 `match` 和 `Result` > > 这里有好多 `match`!`match` 确实很强大,不过也非常的原始。第十三章我们会介绍闭包(closure),它会和定义在 `Result` 中的很多方法一起使用。在处理代码中的 `Result` 值时,相比于使用 `match` ,使用这些方法会更加简洁。 > > 例如,这是另一个编写与示例 9-5 逻辑相同但是使用闭包和 `unwrap_or_else` 方法的例子: > > ```rust,ignore > 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); > } > }); > } > ``` > > 虽然这段代码有着如示例 9-5 一样的行为,但并没有包含任何 `match` 表达式且更容易阅读。在阅读完第十三章后再回到这个例子,并查看标准库文档 `unwrap_or_else` 方法都做了什么操作。在处理错误时,还有很多这类方法可以消除大量嵌套的 `match` 表达式。 ### 失败时 panic 的简写:`unwrap` 和 `expect` `match` 能够胜任它的工作,不过它可能有点冗长并且不总是能很好的表明其意图。`Result` 类型定义了很多辅助方法来处理各种情况。其中之一叫做 `unwrap`,它的实现就类似于示例 9-4 中的 `match` 语句。如果 `Result` 值是成员 `Ok`,`unwrap` 会返回 `Ok` 中的值。如果 `Result` 是成员 `Err`,`unwrap` 会为我们调用 `panic!`。这里是一个实践 `unwrap` 的例子: 文件名:src/main.rs ```rust,should_panic {{#rustdoc_include ../listings/ch09-error-handling/no-listing-04-unwrap/src/main.rs}} ``` 如果调用这段代码时不存在 *hello.txt* 文件,我们将会看到一个 `unwrap` 调用 `panic!` 时提供的错误信息: ```text 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 ``` 还有另一个类似于 `unwrap` 的方法它还允许我们选择 `panic!` 的错误信息:`expect`。使用 `expect` 而不是 `unwrap` 并提供一个好的错误信息可以表明你的意图并更易于追踪 panic 的根源。`expect` 的语法看起来像这样: 文件名:src/main.rs ```rust,should_panic {{#rustdoc_include ../listings/ch09-error-handling/no-listing-05-expect/src/main.rs}} ``` `expect` 与 `unwrap` 的使用方式一样:返回文件句柄或调用 `panic!` 宏。`expect` 在调用 `panic!` 时使用的错误信息将是我们传递给 `expect` 的参数,而不像 `unwrap` 那样使用默认的 `panic!` 信息。它看起来像这样: ```text thread 'main' panicked at 'hello.txt should be included in this project: Error { repr: Os { code: 2, message: "No such file or directory" } }', src/libcore/result.rs:906:4 ``` 在生产级别的代码中,大部分 Rustaceans 选择 `expect` 而不是 `unwrap` 并提供更多关于为何操作期望是一直成功的上下文。如此如果该假设真的被证明是错的,你也有更多的信息来用于调试。 ### 传播错误 当编写一个其实先会调用一些可能会失败的操作的函数时,除了在这个函数中处理错误外,还可以选择让调用者知道这个错误并决定该如何处理。这被称为 **传播**(*propagating*)错误,这样能更好的控制代码调用,因为比起你代码所拥有的上下文,调用者可能拥有更多信息或逻辑来决定应该如何处理错误。 例如,示例 9-6 展示了一个从文件中读取用户名的函数。如果文件不存在或不能读取,这个函数会将这些错误返回给调用它的代码: 文件名:src/main.rs ```rust {{#include ../listings/ch09-error-handling/listing-09-06/src/main.rs:here}} ``` 示例 9-6:一个函数使用 `match` 将错误返回给代码调用者 这个函数可以编写成更加简短的形式,不过我们以大量手动处理开始以便探索错误处理;在最后我们会展示更短的形式。让我们看看函数的返回值:`Result`。这意味着函数返回一个 `Result` 类型的值,其中泛型参数 `T` 的具体类型是 `String`,而 `E` 的具体类型是 `io::Error`。 如果这个函数没有出任何错误成功返回,函数的调用者会收到一个包含 `String` 的 `Ok` 值 —— 函数从文件中读取到的用户名。如果函数遇到任何错误,函数的调用者会收到一个 `Err` 值,它储存了一个包含更多这个问题相关信息的 `io::Error` 实例。这里选择 `io::Error` 作为函数的返回值是因为它正好是函数体中那两个可能会失败的操作的错误返回值:`File::open` 函数和 `read_to_string` 方法。 函数体以调用 `File::open` 函数开始。接着使用 `match` 处理返回值 `Result`,类似示例 9-4,如果 `File::open` 成功了,模式变量 `file` 中的文件句柄就变成了可变变量 `username_file` 中的值,接着函数继续执行。在 `Err` 的情况下,我们没有调用 `panic!`,而是使用 `return` 关键字提前结束整个函数,并将来自 `File::open` 的错误值(现在在模式变量 `e` 中)作为函数的错误值传回给调用者。 所以,如果在 `username_file` 中有一个文件句柄,该函数随后会在变量 `username` 中创建一个新的 `String` 并调用文件句柄 `username_file` 上的 `read_to_string` 方法,以将文件的内容读入 `username`。`read_to_string` 方法也返回一个 `Result`,因为它可能会失败,哪怕是 `File::open` 已经成功了。因此,我们需要另一个 `match` 来处理这个 `Result`:如果 `read_to_string` 执行成功,那么这个函数也就成功了,我们将从文件中读取的用户名返回,此时用户名位于被封装进 `Ok` 的 `username` 中。如果 `read_to_string` 执行失败,则像之前处理 `File::open` 的返回值的 `match` 那样返回错误值。然而,我们无需显式调用 `return` 语句,因为这是函数的最后一个表达式。 调用这个函数的代码最终会得到一个包含用户名的 `Ok` 值,或者一个包含 `io::Error` 的 `Err` 值。我们无从得知调用者会如何处理这些值。例如,如果他们得到了一个 `Err` 值,他们可能会选择 `panic!` 并使程序崩溃、使用一个默认的用户名或者从文件之外的地方寻找用户名。我们没有足够的信息知晓调用者具体会如何尝试,所以将所有的成功或失败信息向上传播,让他们选择合适的处理方法。 这种传播错误的模式在 Rust 是如此的常见,以至于 Rust 提供了 `?` 问号运算符来使其更易于处理。 ### 传播错误的简写:`?` 运算符 示例 9-7 展示了一个 `read_username_from_file` 的实现,它实现了与示例 9-6 中的代码相同的功能,不过这个实现使用了 `?` 运算符: 文件名:src/main.rs ```rust {{#include ../listings/ch09-error-handling/listing-09-07/src/main.rs:here}} ``` 示例 9-7:一个使用 `?` 运算符向调用者返回错误的函数 `Result` 值之后的 `?` 被定义为与示例 9-6 中定义的处理 `Result` 值的 `match` 表达式有着完全相同的工作方式。如果 `Result` 的值是 `Ok`,这个表达式将会返回 `Ok` 中的值而程序将继续执行。如果值是 `Err`,`Err` 将作为整个函数的返回值,就好像使用了 `return` 关键字一样,这样错误值就被传播给了调用者。 示例 9-6 中的 `match` 表达式与 `?` 运算符所做的有一点不同:`?` 运算符所使用的错误值被传递给了 `from` 函数,它定义于标准库的 `From` trait 中,其用来将错误从一种类型转换为另一种类型。当 `?` 运算符调用 `from` 函数时,收到的错误类型被转换为由当前函数返回类型所指定的错误类型。这在当函数返回单个错误类型来代表所有可能失败的方式时很有用,即使其可能会因很多种原因失败。 例如,我们可以将示例 9-7 中的 `read_username_from_file` 函数修改为返回一个自定义的 `OurError` 错误类型。如果我们也定义了 `impl From for OurError` 来从 `io::Error` 构造一个 `OurError` 实例,那么 `read_username_from_file` 函数体中的 `?` 运算符调用会调用 `from` 并转换错误而无需在函数中增加任何额外的代码。 在示例 9-7 的上下文中,`File::open` 调用结尾的 `?` 会将 `Ok` 中的值返回给变量 `username_file`。如果发生了错误,`?` 运算符会使整个函数提前返回并将任何 `Err` 值返回给调用代码。同理也适用于 `read_to_string` 调用结尾的 `?`。 `?` 运算符消除了大量样板代码并使得函数的实现更简单。我们甚至可以在 `?` 之后直接使用链式方法调用来进一步缩短代码,如示例 9-8 所示: 文件名:src/main.rs ```rust {{#include ../listings/ch09-error-handling/listing-09-08/src/main.rs:here}} ``` 示例 9-8:问号运算符之后的链式方法调用 在 `username` 中创建新的 `String` 被放到了函数开头;这一部分没有变化。我们对 `File::open("hello.txt")?` 的结果直接链式调用了 `read_to_string`,而不再创建变量 `username_file`。仍然需要 `read_to_string` 调用结尾的 `?`,而且当 `File::open` 和 `read_to_string` 都成功没有失败时返回包含用户名 `username` 的 `Ok` 值。其功能再一次与示例 9-6 和示例 9-7 保持一致,不过这是一个与众不同且更符合工程学(ergonomic)的写法。 示例 9-9 展示了一个使用 `fs::read_to_string` 的更为简短的写法: 文件名:src/main.rs ```rust {{#include ../listings/ch09-error-handling/listing-09-09/src/main.rs:here}} ``` 示例 9-9: 使用 `fs::read_to_string` 而不是打开后读取文件 将文件读取到一个字符串是相当常见的操作,所以 Rust 提供了名为 `fs::read_to_string` 的函数,它会打开文件、新建一个 `String`、读取文件的内容,并将内容放入 `String`,接着返回它。当然,这样做就没有展示所有这些错误处理的机会了,所以我们最初就选择了艰苦的道路。 ### 哪里可以使用 `?` 运算符 `?` 运算符只能被用于返回值与 `?` 作用的值相兼容的函数。因为 `?` 运算符被定义为从函数中提早返回一个值,这与示例 9-6 中的 `match` 表达式有着完全相同的工作方式。示例 9-6 中 `match` 作用于一个 `Result` 值,提早返回的分支返回了一个 `Err(e)` 值。函数的返回值必须是 `Result` 才能与这个 `return` 相兼容。 在示例 9-10 中,让我们看看在返回值不兼容的 `main` 函数中使用 `?` 运算符会得到什么错误: 文件名:src/main.rs ```rust,ignore,does_not_compile {{#rustdoc_include ../listings/ch09-error-handling/listing-09-10/src/main.rs}} ``` 示例 9-10: 尝试在返回 `()` 的 `main` 函数中使用 `?` 的代码不能编译 这段代码打开一个文件,这可能会失败。`?` 运算符作用于 `File::open` 返回的 `Result` 值,不过 `main` 函数的返回类型是 `()` 而不是 `Result`。当编译这些代码,会得到如下错误信息: ```console {{#include ../listings/ch09-error-handling/listing-09-10/output.txt}} ``` 这个错误指出只能在返回 `Result` 或者其它实现了 `FromResidual` 的类型的函数中使用 `?` 运算符。 为了修复这个错误,有两个选择。一个是,如果没有限制的话将函数的返回值改为 `Result`。另一个是使用 `match` 或 `Result` 的方法中合适的一个来处理 `Result`。 错误信息也提到 `?` 也可用于 `Option` 值。如同对 `Result` 使用 `?` 一样,只能在返回 `Option` 的函数中对 `Option` 使用 `?`。在 `Option` 上调用 `?` 运算符的行为与 `Result` 类似:如果值是 `None`,此时 `None` 会从函数中提前返回。如果值是 `Some`,`Some` 中的值作为表达式的返回值同时函数继续。示例 9-11 中有一个从给定文本中返回第一行最后一个字符的函数的例子: ```rust {{#rustdoc_include ../listings/ch09-error-handling/listing-09-11/src/main.rs:here}} ``` 示例 9-11: 在 `Option` 值上使用 `?` 运算符 这个函数返回 `Option` 因为它可能会在这个位置找到一个字符,也可能没有字符。这段代码获取 `text` 字符串 slice 作为参数并调用其 `lines` 方法,这会返回一个字符串中每一行的迭代器。因为函数希望检查第一行,所以调用了迭代器 `next` 来获取迭代器中第一个值。如果 `text` 是空字符串,`next` 调用会返回 `None`,此时我们可以使用 `?` 来停止并从 `last_char_of_first_line` 返回 `None`。如果 `text` 不是空字符串,`next` 会返回一个包含 `text` 中第一行的字符串 slice 的 `Some` 值。 `?` 会提取这个字符串 slice,然后可以在字符串 slice 上调用 `chars` 来获取字符的迭代器。我们感兴趣的是第一行的最后一个字符,所以可以调用 `last` 来返回迭代器的最后一项。这是一个 `Option`,因为有可能第一行是一个空字符串,例如 `text` 以一个空行开头而后面的行有文本,像是 `"\nhi"`。不过,如果第一行有最后一个字符,它会返回在一个 `Some` 成员中。`?` 运算符作用于其中给了我们一个简洁的表达这种逻辑的方式。如果我们不能在 `Option` 上使用 `?` 运算符,则不得不使用更多的方法调用或者 `match` 表达式来实现这些逻辑。 注意你可以在返回 `Result` 的函数中对 `Result` 使用 `?` 运算符,可以在返回 `Option` 的函数中对 `Option` 使用 `?` 运算符,但是不可以混合搭配。`?` 运算符不会自动将 `Result` 转化为 `Option`,反之亦然;在这些情况下,可以使用类似 `Result` 的 `ok` 方法或者 `Option` 的 `ok_or` 方法来显式转换。 目前为止,我们所使用的所有 `main` 函数都返回 `()`。`main` 函数是特殊的因为它是可执行程序的入口点和退出点,为了使程序能正常工作,其可以返回的类型是有限制的。 幸运的是 `main` 函数也可以返回 `Result<(), E>`,示例 9-12 中的代码来自示例 9-10 不过修改了 `main` 的返回值为 `Result<(), Box>` 并在结尾增加了一个 `Ok(())` 作为返回值。这段代码可以编译: ```rust,ignore {{#rustdoc_include ../listings/ch09-error-handling/listing-09-12/src/main.rs}} ``` 示例 9-12: 修改 `main` 返回 `Result<(), E>` 允许对 `Result` 值使用 `?` 运算符 `Box` 类型是一个 **trait 对象**(*trait object*)第十八章 [顾及不同类型值的 trait 对象”][trait-objects] 部分会做介绍。目前可以将 `Box` 理解为 “任何类型的错误”。在返回 `Box` 错误类型 `main` 函数中对 `Result` 使用 `?` 是允许的,因为它允许任何 `Err` 值提前返回。即便 `main` 函数体从来只会返回 `std::io::Error` 错误类型,通过指定 `Box`,这个签名也仍是正确的,甚至当 `main` 函数体中增加更多返回其他错误类型的代码时也是如此。 当 `main` 函数返回 `Result<(), E>`,如果 `main` 返回 `Ok(())` 可执行程序会以 `0` 值退出,而如果 `main` 返回 `Err` 值则会以非零值退出;成功退出的程序会返回整数 `0`,运行错误的程序会返回非 `0` 的整数。Rust 也会从二进制程序中返回与这个惯例相兼容的整数。 `main` 函数也可以返回任何实现了 [`std::process::Termination` trait][termination] 的类型,它包含了一个返回 `ExitCode` 的 `report` 函数。请查阅标准库文档了解更多为自定义类型实现 `Termination` trait 的细节。 现在我们讨论过了调用 `panic!` 或返回 `Result` 的细节,是时候回到它们各自适合哪些场景的话题了。 [handle_failure]: ch02-00-guessing-game-tutorial.html#使用-result-类型来处理潜在的错误 [trait-objects]: ch18-02-trait-objects.html#顾及不同类型值的-trait-对象 [termination]: https://doc.rust-lang.org/std/process/trait.Termination.html