可恢复的错误与Result
大多数错误并不严重到需要程序完全停止。
有时,当一个函数失败时,原因是你很容易解释并作出反应的。例如,如果你尝试打开一个文件,而该操作失败是因为文件不存在,你可能希望创建该文件而不是终止进程。
回想在第 2 章的 “使用 Result
处理潜在失败” 中,Result
枚举被定义为具有两个变体,Ok
和 Err
,如下所示:
#![allow(unused)] fn main() { enum Result<T, E> { Ok(T), Err(E), } }
The T
和 E
是泛型类型参数:我们将在第 10 章中更详细地讨论泛型。你现在需要知道的是,T
代表在 Ok
变体中成功情况下将返回的值的类型,而 E
代表在 Err
变体中失败情况下将返回的错误的类型。因为 Result
有这些泛型类型参数,我们可以在许多不同的情况下使用 Result
类型和定义在其上的函数,这些情况下我们想要返回的成功值和错误值可能不同。
让我们调用一个返回 Result
值的函数,因为该函数可能会失败。在示例 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 展示了一种使用基本工具 match
表达式处理 Result
的方法,我们在第 6 章中讨论过这个表达式。
注意,像 Option
枚举一样,Result
枚举及其变体已被预导言引入作用域,因此我们无需在 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
如常,此输出确切地告诉我们出了什么问题。
匹配不同的错误
列表 9-4 中的代码无论 File::open
为何失败都会 panic!
。然而,我们希望根据不同的失败原因采取不同的行动。如果 File::open
失败是因为文件不存在,我们希望创建该文件并返回新文件的句柄。如果 File::open
因其他原因失败——例如,因为我们没有权限打开文件——我们仍然希望代码像在列表 9-4 中那样 panic!
。为此,我们添加了一个内部的 match
表达式,如列表 9-5 所示。
File::open
在 Err
变体中返回的值的类型是 io::Error
,这是一个由标准库提供的结构体。这个结构体有一个 kind
方法,我们可以调用它来获取一个 io::ErrorKind
值。枚举 io::ErrorKind
由标准库提供,具有表示可能从 io
操作中产生的不同类型的错误的变体。我们想要使用的变体是 ErrorKind::NotFound
,它表示我们试图打开的文件还不存在。因此,我们对 greeting_file_result
进行匹配,但我们也对 error.kind()
进行内部匹配。
我们在内部 match
中要检查的条件是 error.kind()
返回的值是否是 ErrorKind
枚举的 NotFound
变体。如果是,我们尝试使用 File::create
创建文件。然而,因为 File::create
也可能失败,我们需要在内部 match
表达式中添加第二个分支。当文件无法创建时,会打印不同的错误消息。外部 match
的第二个分支保持不变,因此程序会在除文件缺失错误之外的任何错误上崩溃。
Alternatives to Using match
with Result<T, E>
那是很多 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
表达式。
Shortcuts for Panic on Error: unwrap
and expect
使用match
效果很好,但有时会显得有些冗长,并且不总是能很好地传达意图。Result<T, E>
类型定义了许多辅助方法,用于执行各种更具体的任务。unwrap
方法是一个快捷方法,实现方式与我们在清单9-4中编写的match
表达式相同。如果Result
值是Ok
变体,unwrap
将返回Ok
中的值。如果Result
是Err
变体,unwrap
将为我们调用panic!
宏。以下是一个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
并提供良好的错误消息可以传达
您的意图,并使追踪恐慌的来源更容易。 expect
的语法如下:
我们以与 unwrap
相同的方式使用 expect
:返回文件句柄或调用 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 显示了一个从文件中读取用户名的函数。如果文件不存在或无法读取,此函数将返回这些错误给调用该函数的代码。
这个函数可以用更短的方式编写,但我们将从手动完成很多工作开始,以便探索错误处理;最后,我们将展示更短的方式。让我们先看看函数的返回类型:Result<String, io::Error>
。这意味着函数返回一个类型为Result<T, E>
的值,其中泛型参数T
已被具体类型String
填充,泛型类型E
已被具体类型io::Error
填充。
如果这个函数成功执行没有任何问题,调用这个函数的代码将收到一个Ok
值,其中包含一个String
——即这个函数从文件中读取的username
。如果这个函数遇到任何问题,调用代码将收到一个Err
值,其中包含一个io::Error
实例,该实例包含有关问题的更多信息。我们选择io::Error
作为此函数的返回类型,因为这恰好是我们在这个函数体中调用的两个可能失败的操作返回的错误值类型:File::open
函数和read_to_string
方法。
函数体首先调用 File::open
函数。然后我们使用一个类似于列表 9-4 中的 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
成功,那么我们的函数就成功了,我们返回一个包含在 Ok
中的文件中的用户名,该用户名现在在 username
中。如果 read_to_string
失败,我们以与处理 File::open
返回值的 match
中返回错误值相同的方式返回错误值。然而,我们不需要显式地说 return
,因为这是函数中的最后一个表达式。
调用此代码的代码将处理获取一个包含用户名的Ok
值或包含一个io::Error
的Err
值。由调用代码决定如何处理这些值。如果调用代码获取到一个Err
值,它可以调用panic!
并使程序崩溃,使用默认用户名,或者从文件以外的其他地方查找用户名,例如。我们没有足够的信息来了解调用代码实际试图做什么,所以我们向上传递所有成功或错误信息,以便它能够适当地处理。
这种传播错误的模式在 Rust 中非常常见,以至于 Rust 提供了问号操作符 ?
来使这变得更加容易。
A Shortcut for Propagating Errors: the ?
Operator
列表 9-7 显示了 read_username_from_file
的一个实现,该实现与列表 9-6 中的功能相同,但此实现使用了 ?
操作符。
放置在 Result
值后面的 ?
被定义为几乎以与我们在清单 9-6 中定义的处理 Result
值的 match
表达式相同的方式工作。如果 Result
的值是 Ok
,则 Ok
内的值将从这个表达式返回,程序将继续执行。如果值是 Err
,则 Err
将从整个函数返回,就像我们使用了 return
关键字一样,因此错误值将传播到调用代码。
match
表达式(来自清单 9-6)和 ?
操作符之间存在差异:调用了 ?
操作符的错误值会通过标准库中定义的 From
特性中的 from
函数进行处理,该函数用于将值从一种类型转换为另一种类型。当 ?
操作符调用 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 所示。
我们将创建新的String
在username
中的代码移到了函数的开头;那部分没有改变。我们没有创建一个变量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
使这更短的方法。
将文件读取到字符串中是一个相当常见的操作,因此标准库提供了方便的fs::read_to_string
函数,该函数打开文件,创建一个新的String
,读取文件内容,将内容放入该String
中,并返回它。当然,使用fs::read_to_string
不会给我们解释所有错误处理的机会,所以我们先用了更长的方法。
Where The ?
Operator Can Be Used
?
操作符只能用于返回类型与 ?
操作符所使用的值兼容的函数中。这是因为 ?
操作符被定义为以与我们在清单 9-6 中定义的 match
表达式相同的方式,提前从函数中返回一个值。在清单 9-6 中,match
使用了一个 Result
值,提前返回的分支返回了一个 Err(e)
值。函数的返回类型必须是 Result
,以便与这个 return
兼容。
在清单 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 + Ok(())
|
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<T>
上调用 ?
操作符时,其行为类似于在 Result<T, E>
上调用时的行为:如果值是 None
,则在该点函数会提前返回 None
。如果值是 Some
,则 Some
内的值是表达式的结果值,函数继续执行。列表 9-11 有一个示例函数,该函数查找给定文本中第一行的最后一个字符。
这个函数返回 Option<char>
,因为可能存在一个字符,但也可能不存在。这段代码接受 text
字符串切片参数,并在其上调用 lines
方法,该方法返回一个遍历字符串中各行的迭代器。因为这个函数想要检查第一行,所以它在迭代器上调用 next
以获取迭代器中的第一个值。如果 text
是空字符串,这次对 next
的调用将返回 None
,在这种情况下我们使用 ?
来停止并从 last_char_of_first_line
返回 None
。如果 text
不是空字符串,next
将返回一个包含 text
中第一行字符串切片的 Some
值。
?
提取字符串切片,我们可以在该字符串切片上调用 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(())
。此代码现在可以编译。
Box<dyn Error>
类型是一个 特征对象,我们将在第 18 章的
“使用允许不同类型值的特征对象” 部分讨论。目前,你可以将 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
函数可以返回任何实现了 the std::process::Termination
特性 的类型,其中包含一个返回 ExitCode
的函数 report
。有关为您的类型实现 Termination
特性的更多信息,请参阅标准库文档。
现在我们已经讨论了调用panic!
或返回Result
的细节,让我们回到如何决定在哪些情况下使用哪个主题。