使用 Result
处理可恢复的错误
大多数错误并不严重到需要程序完全停止。有时,当函数失败时,其原因很容易解释和响应。例如,如果您尝试打开一个文件,但由于该文件不存在而导致操作失败,您可能希望创建该文件而不是终止进程。
回想一下第二章的“使用 Result
处理潜在的失败”,Result
枚举被定义为具有两个变体,Ok
和 Err
,如下所示:
#![allow(unused)] fn main() { enum Result<T, E> { Ok(T), Err(E), } }
T
和 E
是泛型类型参数:我们将在第 10 章中更详细地讨论泛型。您现在需要知道的是,T
表示 Ok
变体中成功情况下将返回的值的类型,而 E
表示 Err
变体中失败情况下将返回的错误的类型。由于 Result
具有这些泛型类型参数,我们可以在许多不同的情况下使用 Result
类型和在其上定义的函数,在这些情况下,我们希望返回的成功值和错误值可能会有所不同。
让我们调用一个返回 Result
值的函数,因为该函数可能会失败。在清单 9-3 中,我们尝试打开一个文件。
文件名:src/main.rs
use std::fs::File; fn main() { let greeting_file_result = File::open("hello.txt"); }
清单 9-3:打开一个文件
File::open
的返回类型是 Result<T, E>
。泛型参数 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 展示了使用基本工具(我们在第 6 章中讨论过的 match
表达式)处理 Result
的一种方法。
文件名:src/main.rs
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:?}"), }; }
清单 9-4:使用 match
表达式处理可能返回的 Result
变体
请注意,与 Option
枚举一样,Result
枚举及其变体已由 prelude 引入范围,因此我们不需要在 match
分支中在 Ok
和 Err
变体之前指定 Result::
。
当结果为 Ok
时,此代码将从 Ok
变体中返回内部的 file
值,然后我们将该文件句柄值分配给变量 greeting_file
。在 match
之后,我们可以使用文件句柄进行读取或写入。
match
的另一个分支处理我们从 File::open
获得 Err
值的情况。在此示例中,我们选择调用 panic!
宏。如果我们的当前目录中没有名为 *hello.txt* 的文件,并且我们运行此代码,我们将看到 panic!
宏的以下输出
$ cargo run
Compiling error-handling v0.1.0 (file:///projects/error-handling)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.73s
Running `target/debug/error-handling`
thread 'main' panicked at src/main.rs:8:23:
Problem opening the file: Os { code: 2, kind: NotFound, message: "No such file or directory" }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
像往常一样,此输出会准确地告诉我们哪里出了问题。
匹配不同的错误
无论 File::open
失败的原因是什么,清单 9-4 中的代码都会 panic!
。但是,我们希望针对不同的失败原因采取不同的操作。如果 File::open
失败是因为该文件不存在,我们希望创建该文件并返回新文件的句柄。如果 File::open
因任何其他原因失败(例如,因为我们没有打开该文件的权限),我们仍然希望代码以与清单 9-4 中相同的方式 panic!
。为此,我们添加一个内部 match
表达式,如清单 9-5 所示。
文件名:src/main.rs
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:?}");
}
},
};
}
清单 9-5:以不同的方式处理不同类型的错误
File::open
在 Err
变体内部返回的值的类型是 io::Error
,它是标准库提供的结构体。此结构体有一个我们可以调用的 kind
方法来获取 io::ErrorKind
值。枚举 io::ErrorKind
由标准库提供,并具有表示可能由 io
操作产生的不同类型的错误的变体。我们要使用的变体是 ErrorKind::NotFound
,它指示我们尝试打开的文件尚不存在。因此,我们匹配 greeting_file_result
,但我们也在 error.kind()
上进行了内部匹配。
我们想在内部匹配中检查的条件是 error.kind()
返回的值是否是 ErrorKind
枚举的 NotFound
变体。如果是,我们尝试使用 File::create
创建文件。但是,由于 File::create
也可能失败,因此我们需要内部 match
表达式中的第二个分支。当无法创建文件时,会打印不同的错误消息。外部 match
的第二个分支保持不变,因此程序会在缺少文件错误以外的任何错误上 panic。
使用 Result<T, E>
的 match
的替代方案
这有很多 match
! match
表达式非常有用,但也非常原始。在第 13 章中,您将学习闭包,闭包与 Result<T, E>
上定义的许多方法一起使用。当在代码中处理 Result<T, E>
值时,这些方法可以比使用 match
更简洁。
例如,以下是编写与清单 9-5 中相同的逻辑的另一种方法,这次使用闭包和 unwrap_or_else
方法
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
表达式,并且更易于阅读。在您阅读完第 13 章后,请回到此示例,并在标准库文档中查找 unwrap_or_else
方法。当您处理错误时,这些方法中的更多方法可以清除巨大的嵌套 match
表达式。
发生错误时 panic 的快捷方式:unwrap
和 expect
使用 match
效果很好,但它可能有点冗长,并且并不总是能很好地传达意图。Result<T, E>
类型在其上定义了许多辅助方法来执行各种更具体的任务。unwrap
方法是一个快捷方法,其实现方式与我们在清单 9-4 中编写的 match
表达式相同。如果 Result
值是 Ok
变体,则 unwrap
将返回 Ok
内部的值。如果 Result
是 Err
变体,则 unwrap
将为我们调用 panic!
宏。以下是 unwrap
的一个实际示例
文件名:src/main.rs
use std::fs::File; fn main() { let greeting_file = File::open("hello.txt").unwrap(); }
如果我们运行此代码而没有 *hello.txt* 文件,我们将看到来自 unwrap
方法进行的 panic!
调用的错误消息
thread 'main' panicked at src/main.rs:4:49:
called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }
类似地,expect
方法也允许我们选择 panic!
错误消息。使用 expect
而不是 unwrap
并提供良好的错误消息可以传达您的意图,并使跟踪 panic 的来源更容易。expect
的语法如下所示
文件名:src/main.rs
use std::fs::File; fn main() { let greeting_file = File::open("hello.txt") .expect("hello.txt should be included in this project"); }
我们使用 expect
的方式与 unwrap
相同:返回文件句柄或调用 panic!
宏。expect
在其对 panic!
的调用中使用的错误消息将是我们传递给 expect
的参数,而不是 unwrap
使用的默认 panic!
消息。这是它的样子
thread 'main' panicked at src/main.rs:5:10:
hello.txt should be included in this project: Os { code: 2, kind: NotFound, message: "No such file or directory" }
在生产质量的代码中,大多数 Rustaceans 选择 expect
而不是 unwrap
,并提供有关为什么该操作应该始终成功的更多上下文。这样,如果您的假设被证明是错误的,您将有更多信息用于调试。
传播错误
当函数的实现调用可能会失败的操作时,您可以将错误返回给调用代码,而不是在函数本身内部处理错误,以便调用代码可以决定该怎么做。这被称为传播错误,它为调用代码提供了更多的控制权,在调用代码中,可能存在更多的信息或逻辑来指示如何处理错误,而不是您在代码的上下文中可用的信息。
例如,清单 9-6 显示了一个从文件中读取用户名的函数。如果文件不存在或无法读取,此函数会将这些错误返回给调用该函数的代码。
文件名:src/main.rs
#![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), } } }
清单 9-6:使用 match
将错误返回给调用代码的函数
可以使用更短的方式编写此函数,但我们将从手动执行许多操作开始,以便探索错误处理;最后,我们将展示更短的方式。让我们首先看一下函数的返回类型:Result<String, io::Error>
。这意味着该函数返回类型为 Result<T, E>
的值,其中泛型参数 T
已填充为具体类型 String
,而泛型类型 E
已填充为具体类型 io::Error
。
如果此函数在没有任何问题的情况下成功,则调用此函数的代码将收到一个包含 String
的 Ok
值 - 此函数从文件中读取的 username
。如果此函数遇到任何问题,则调用代码将收到一个包含 io::Error
实例的 Err
值,其中包含有关问题的更多信息。我们选择 io::Error
作为此函数的返回类型,因为这恰好是从我们在此函数的主体中调用的两个可能失败的操作(File::open
函数和 read_to_string
方法)返回的错误值的类型。
函数的主体首先调用 File::open
函数。然后,我们使用类似于清单 9-4 中的 match
的 match
处理 Result
值。如果 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
成功,那么我们的函数也成功了,并且我们从文件中返回现在位于 username
中的用户名,并将其包裹在 Ok
中。如果 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
#![allow(unused)] fn main() { 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) } }
列表 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<io::Error> 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
#![allow(unused)] fn main() { 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) } }
列表 9-8:在 ?
运算符之后链接方法调用
我们将 username
中新 String
的创建移动到函数的开头;那部分没有改变。我们没有创建变量 username_file
,而是将对 read_to_string
的调用直接链接到 File::open("hello.txt")?
的结果。在 read_to_string
调用的末尾,我们仍然有一个 ?
,并且当 File::open
和 read_to_string
都成功时,我们仍然返回包含 username
的 Ok
值,而不是返回错误。功能与列表 9-6 和列表 9-7 中的相同;这只是一种不同的、更符合人体工程学的编写方式。
列表 9-9 展示了一种使用 fs::read_to_string
使其更短的方法。
文件名:src/main.rs
#![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") } }
列表 9-9:使用 fs::read_to_string
而不是打开然后读取文件
将文件读取到字符串中是一个相当常见的操作,因此标准库提供了方便的 fs::read_to_string
函数,该函数打开文件,创建新的 String
,读取文件的内容,将内容放入该 String
中,然后将其返回。当然,使用 fs::read_to_string
没有给我们解释所有错误处理的机会,所以我们首先以较长的方式进行了说明。
?
运算符可以在哪里使用
?
运算符只能在返回类型与 ?
使用的值兼容的函数中使用。这是因为 ?
运算符被定义为执行从函数中提前返回值的操作,这与我们在列表 9-6 中定义的 match
表达式的方式相同。在列表 9-6 中,match
使用的是 Result
值,并且提前返回的分支返回了 Err(e)
值。函数的返回类型必须是 Result
,以便它与此 return
兼容。
在列表 9-10 中,让我们看看如果在返回类型与我们使用 ?
的值的类型不兼容的 main
函数中使用 ?
运算符,会得到什么错误。
文件名:src/main.rs
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt")?;
}
列表 9-10:尝试在返回 ()
的 main
函数中使用 ?
将无法编译。
此代码打开一个文件,这可能会失败。 ?
运算符遵循 File::open
返回的 Result
值,但是此 main
函数的返回类型为 ()
,而不是 Result
。当我们编译此代码时,会收到以下错误消息
$ 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 `()`
help: consider adding return type
|
3 ~ fn main() -> Result<(), Box<dyn std::error::Error>> {
4 | let greeting_file = File::open("hello.txt")?;
5 +
6 + Ok(())
7 + }
|
For more information about this error, try `rustc --explain E0277`.
error: could not compile `error-handling` (bin "error-handling") due to 1 previous error
此错误指出,我们只允许在返回 Result
、Option
或其他实现 FromResidual
的类型的函数中使用 ?
运算符。
要修复该错误,你有两种选择。一种选择是将函数的返回类型更改为与你使用 ?
运算符的值兼容,只要你没有阻止这种情况的限制。另一种选择是使用 match
或 Result<T, E>
的方法之一以任何适当的方式处理 Result<T, E>
。
错误消息还提到,?
可以与 Option<T>
值一起使用。与在 Result
上使用 ?
一样,你只能在返回 Option
的函数中对 Option
使用 ?
。在 Option<T>
上调用时,?
运算符的行为类似于在 Result<T, E>
上调用时的行为:如果该值为 None
,则 None
将从该点提前从函数返回。如果该值是 Some
,则 Some
内的值是表达式的结果值,并且函数继续。列表 9-11 有一个示例,该示例函数查找给定文本中第一行的最后一个字符。
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); }
列表 9-11:在 Option<T>
值上使用 ?
运算符
此函数返回 Option<char>
,因为那里可能有一个字符,但也可能没有。此代码采用 text
字符串切片参数,并在其上调用 lines
方法,该方法返回一个遍历字符串中行的迭代器。因为此函数要检查第一行,因此它在迭代器上调用 next
以从迭代器中获取第一个值。如果 text
是空字符串,则对此 next
的调用将返回 None
,在这种情况下,我们使用 ?
来停止并从 last_char_of_first_line
返回 None
。如果 text
不是空字符串,则 next
将返回一个 Some
值,其中包含 text
中第一行的字符串切片。
?
提取字符串切片,我们可以对该字符串切片调用 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<dyn Error>>
,并在末尾添加了返回值 Ok(())
。此代码现在可以编译。
文件名:src/main.rs
use std::error::Error;
use std::fs::File;
fn main() -> Result<(), Box<dyn Error>> {
let greeting_file = File::open("hello.txt")?;
Ok(())
}
列表 9-12:将 main
更改为返回 Result<(), E>
允许在 Result
值上使用 ?
运算符。
Box<dyn Error>
类型是一个trait 对象,我们将在“使用允许不同类型值的 trait 对象”中讨论它第17章中的一部分。目前,你可以将 Box<dyn Error>
理解为“任何类型的错误”。在错误类型为 Box<dyn Error>
的 main
函数中使用 ?
操作符处理 Result
值是被允许的,因为它允许提前返回任何 Err
值。即使此 main
函数的主体只会返回 std::io::Error
类型的错误,但通过指定 Box<dyn Error>
,即使在 main
函数的主体中添加了返回其他错误的代码,此签名仍然是正确的。
当 main
函数返回 Result<(), E>
时,如果 main
返回 Ok(())
,则可执行文件将以值 0
退出;如果 main
返回 Err
值,则将以非零值退出。用 C 语言编写的可执行文件在退出时返回整数:成功退出的程序返回整数 0
,而发生错误的程序返回非 0
的整数。Rust 也从可执行文件返回整数,以兼容此约定。
main
函数可以返回任何实现了 std::process::Termination
trait 的类型,该 trait 包含一个返回 ExitCode
的函数 report
。有关为你自己的类型实现 Termination
trait 的更多信息,请查阅标准库文档。
现在我们已经讨论了调用 panic!
或返回 Result
的细节,让我们回到如何决定在哪些情况下使用哪种方式的话题。