用 Result 处理可恢复的错误
Recoverable Errors with Result
大多数错误都不严重,不需要程序完全停止。有时函数失败是由于你可以轻松解释并响应的原因。例如,如果你尝试打开一个文件,但由于文件不存在而失败,你可能希望创建该文件而不是终止进程。
Most errors aren’t serious enough to require the program to stop entirely. Sometimes when a function fails, it’s for a reason that you can easily interpret and respond to. For example, if you try to open a file and that operation fails because the file doesn’t exist, you might want to create the file instead of terminating the process.
回顾第 2 章“使用 Result 处理潜在的失败”,Result 枚举定义如下,它有两个变体:Ok 和 Err:
Recall from “Handling Potential Failure with Result” in Chapter 2 that the Result enum is defined as having two variants, Ok and Err, as follows:
#![allow(unused)]
fn main() {
enum Result<T, E> {
Ok(T),
Err(E),
}
}
T 和 E 是泛型类型参数:我们将在第 10 章详细讨论泛型。你现在需要知道的是,T 代表成功情况下 Ok 变体中将返回的值的类型,而 E 代表失败情况下 Err 变体中将返回的错误的类型。因为 Result 具有这些泛型类型参数,所以我们可以在许多不同的情况下使用 Result 类型及其定义的方法,即使我们想要返回的成功值和错误值可能各不相同。
The T and E are generic type parameters: We’ll discuss generics in more detail in Chapter 10. What you need to know right now is that T represents the type of the value that will be returned in a success case within the Ok variant, and E represents the type of the error that will be returned in a failure case within the Err variant. Because Result has these generic type parameters, we can use the Result type and the functions defined on it in many different situations where the success value and error value we want to return may differ.
让我们调用一个返回 Result 值的函数,因为该函数可能会失败。在示例 9-3 中,我们尝试打开一个文件。
Let’s call a function that returns a Result value because the function could fail. In Listing 9-3, we try to open a file.
use std::fs::File;
fn main() {
let greeting_file_result = File::open("hello.txt");
}
File::open 的返回类型是 Result<T, E>。泛型参数 T 已由 File::open 的实现填充为成功值的类型 std::fs::File,它是一个文件句柄。错误值中使用的 E 的类型是 std::io::Error。这个返回类型意味着对 File::open 的调用可能会成功,并返回一个我们可以进行读写的文件句柄。该函数调用也可能会失败:例如,文件可能不存在,或者我们可能没有访问该文件的权限。File::open 函数需要有一种方式告诉我们它是成功还是失败,同时为我们提供文件句柄或错误信息。这正是 Result 枚举所表达的信息。
The return type of File::open is a Result<T, E>. The generic parameter T has been filled in by the implementation of File::open with the type of the success value, std::fs::File, which is a file handle. The type of E used in the error value is std::io::Error. This return type means the call to File::open might succeed and return a file handle that we can read from or write to. The function call also might fail: For example, the file might not exist, or we might not have permission to access the file. The File::open function needs to have a way to tell us whether it succeeded or failed and at the same time give us either the file handle or error information. This information is exactly what the Result enum conveys.
在 File::open 成功的情况下,变量 greeting_file_result 中的值将是一个包含文件句柄的 Ok 实例。在失败的情况下,greeting_file_result 中的值将是一个包含有关所发生错误种类的更多信息的 Err 实例。
In the case where File::open succeeds, the value in the variable greeting_file_result will be an instance of Ok that contains a file handle. In the case where it fails, the value in greeting_file_result will be an instance of Err that contains more information about the kind of error that occurred.
我们需要在示例 9-3 的代码基础上添加逻辑,根据 File::open 返回的值采取不同的行动。示例 9-4 展示了处理 Result 的一种方法,即使用我们在第 6 章讨论过的基本工具 match 表达式。
We need to add to the code in Listing 9-3 to take different actions depending on the value File::open returns. Listing 9-4 shows one way to handle the Result using a basic tool, the match expression that we discussed in Chapter 6.
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:?}"),
};
}
请注意,与 Option 枚举一样,Result 枚举及其变体已由 prelude 引入作用域,因此我们不需要在 match 分支中的 Ok 和 Err 变体之前指定 Result::。
Note that, like the Option enum, the Result enum and its variants have been brought into scope by the prelude, so we don’t need to specify Result:: before the Ok and Err variants in the match arms.
当结果为 Ok 时,这段代码将从 Ok 变体中返回内部的 file 值,然后我们将该文件句柄值分配给变量 greeting_file。在 match 之后,我们可以使用该文件句柄进行读写。
When the result is Ok, this code will return the inner file value out of the Ok variant, and we then assign that file handle value to the variable greeting_file. After the match, we can use the file handle for reading or writing.
match 的另一个分支处理我们从 File::open 获得 Err 值的情况。在这个例子中,我们选择调用 panic! 宏。如果当前目录中没有名为 hello.txt 的文件并运行此代码,我们将看到来自 panic! 宏的以下输出:
The other arm of the match handles the case where we get an Err value from File::open. In this example, we’ve chosen to call the panic! macro. If there’s no file named hello.txt in our current directory and we run this code, we’ll see the following output from the panic! macro:
$ 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
一如既往,此输出准确地告诉我们出了什么问题。
As usual, this output tells us exactly what has gone wrong.
匹配不同的错误
Matching on Different Errors
无论 File::open 失败的原因是什么,示例 9-4 中的代码都会执行 panic!。然而,我们希望针对不同的失败原因采取不同的行动。如果 File::open 因为文件不存在而失败,我们希望创建该文件并返回新文件的句柄。如果 File::open 因为任何其他原因(例如,因为我们没有打开该文件的权限)而失败,我们仍然希望代码像示例 9-4 中那样执行 panic!。为此,我们添加了一个内部 match 表达式,如示例 9-5 所示。
The code in Listing 9-4 will panic! no matter why File::open failed. However, we want to take different actions for different failure reasons. If File::open failed because the file doesn’t exist, we want to create the file and return the handle to the new file. If File::open failed for any other reason—for example, because we didn’t have permission to open the file—we still want the code to panic! in the same way it did in Listing 9-4. For this, we add an inner match expression, shown in Listing 9-5.
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:?}"),
},
_ => {
panic!("Problem opening the file: {error:?}");
}
},
};
}
File::open 在 Err 变体中返回的值类型是 io::Error,这是标准库提供的一个结构体。该结构体有一个 kind 方法,我们可以调用它来获取 io::ErrorKind 值。枚举 io::ErrorKind 由标准库提供,其变体代表可能由 io 操作导致的不同种类的错误。我们要使用的变体是 ErrorKind::NotFound,它表示我们要尝试打开的文件尚不存在。因此,我们对 greeting_file_result 进行匹配,但在内部也对 error.kind() 进行匹配。
The type of the value that File::open returns inside the Err variant is io::Error, which is a struct provided by the standard library. This struct has a method, kind, that we can call to get an io::ErrorKind value. The enum io::ErrorKind is provided by the standard library and has variants representing the different kinds of errors that might result from an io operation. The variant we want to use is ErrorKind::NotFound, which indicates the file we’re trying to open doesn’t exist yet. So, we match on greeting_file_result, but we also have an inner match on error.kind().
我们想在内部匹配中检查的条件是 error.kind() 返回的值是否是 ErrorKind 枚举的 NotFound 变体。如果是,我们尝试使用 File::create 创建文件。然而,因为 File::create 也可能会失败,所以我们需要在内部 match 表达式中添加第二个分支。当文件无法创建时,会打印不同的错误消息。外部 match 的第二个分支保持不变,因此除了文件缺失错误之外,程序在遇到任何其他错误时都会发生恐慌。
The condition we want to check in the inner match is whether the value returned by error.kind() is the NotFound variant of the ErrorKind enum. If it is, we try to create the file with File::create. However, because File::create could also fail, we need a second arm in the inner match expression. When the file can’t be created, a different error message is printed. The second arm of the outer match stays the same, so the program panics on any error besides the missing file error.
使用
Result<T, E>时 match 的替代方案
Alternatives to Using
matchwithResult<T, E>这么多的
match!match表达式非常有用,但也很原始。在第 13 章中,你将学习闭包,它与Result<T, E>上定义的许多方法配合使用。在处理代码中的Result<T, E>值时,这些方法可以比使用match更简洁。That’s a lot of
match! Thematchexpression is very useful but also very much a primitive. In Chapter 13, you’ll learn about closures, which are used with many of the methods defined onResult<T, E>. These methods can be more concise than usingmatchwhen handlingResult<T, E>values in your code.例如,这里是编写与示例 9-5 相同逻辑的另一种方式,这次使用了闭包和
unwrap_or_else方法:For example, here’s another way to write the same logic as shown in Listing 9-5, this time using closures and the
unwrap_or_elsemethod: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表达式。Although this code has the same behavior as Listing 9-5, it doesn’t contain any
matchexpressions and is cleaner to read. Come back to this example after you’ve read Chapter 13 and look up theunwrap_or_elsemethod in the standard library documentation. Many more of these methods can clean up huge, nestedmatchexpressions when you’re dealing with errors.
遇到错误时引发恐慌的简捷方法
Shortcuts for Panic on Error
使用 match 工作得很好,但它可能有点冗长,而且并不总能很好地传达意图。Result<T, E> 类型定义了许多辅助方法来执行各种更具体的任务。unwrap 方法是一个简捷方法,它的实现就像我们在示例 9-4 中编写的 match 表达式一样。如果 Result 值是 Ok 变体,unwrap 将返回 Ok 内部的值。如果 Result 是 Err 变体,unwrap 将为我们调用 panic! 宏。以下是 unwrap 实际应用的一个例子:
Using match works well enough, but it can be a bit verbose and doesn’t always communicate intent well. The Result<T, E> type has many helper methods defined on it to do various, more specific tasks. The unwrap method is a shortcut method implemented just like the match expression we wrote in Listing 9-4. If the Result value is the Ok variant, unwrap will return the value inside the Ok. If the Result is the Err variant, unwrap will call the panic! macro for us. Here is an example of unwrap in action:
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt").unwrap();
}
如果我们在没有 hello.txt 文件的情况下运行此代码,我们将看到来自 unwrap 方法发出的 panic! 调用的错误消息:
If we run this code without a hello.txt file, we’ll see an error message from the panic! call that the unwrap method makes:
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 的语法如下所示:
Similarly, the expect method lets us also choose the panic! error message. Using expect instead of unwrap and providing good error messages can convey your intent and make tracking down the source of a panic easier. The syntax of expect looks like this:
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt")
.expect("hello.txt should be included in this project");
}
我们以与 unwrap 相同的方式使用 expect:返回文件句柄或调用 panic! 宏。expect 在其 panic! 调用中使用的错误消息将是我们传递给 expect 的参数,而不是 unwrap 使用的默认 panic! 消息。它看起来像这样:
We use expect in the same way as unwrap: to return the file handle or call the panic! macro. The error message used by expect in its call to panic! will be the parameter that we pass to expect, rather than the default panic! message that unwrap uses. Here’s what it looks like:
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" }
在具有生产质量的代码中,大多数 Rust 用户会选择 expect 而不是 unwrap,并提供更多关于为什么该操作被预期为始终成功的信息。这样,如果你的假设最终被证明是错误的,你就有更多的信息用于调试。
In production-quality code, most Rustaceans choose expect rather than unwrap and give more context about why the operation is expected to always succeed. That way, if your assumptions are ever proven wrong, you have more information to use in debugging.
传播错误
Propagating Errors
当函数的实现调用了某些可能会失败的操作时,你不需要在函数内部处理错误,而是可以将错误返回给调用代码,以便它决定如何处理。这被称为 传播(propagating)错误,并将更多控制权交给了调用代码,因为与你的代码上下文相比,调用代码可能拥有更多引导错误处理的信息或逻辑。
When a function’s implementation calls something that might fail, instead of handling the error within the function itself, you can return the error to the calling code so that it can decide what to do. This is known as propagating the error and gives more control to the calling code, where there might be more information or logic that dictates how the error should be handled than what you have available in the context of your code.
例如,示例 9-6 展示了一个从文件中读取用户名的函数。如果文件不存在或无法读取,该函数将把这些错误返回给调用该函数的代码。
For example, Listing 9-6 shows a function that reads a username from a file. If the file doesn’t exist or can’t be read, this function will return those errors to the code that called the function.
#![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),
}
}
}
这个函数可以用更简短的方式编写,但为了探索错误处理,我们将从手动完成大部分工作开始;最后,我们将展示简短的方式。让我们先看看函数的返回类型:Result<String, io::Error>。这意味着该函数返回一个 Result<T, E> 类型的值,其中泛型参数 T 已填充为具体类型 String,泛型参数 E 已填充为具体类型 io::Error。
This function can be written in a much shorter way, but we’re going to start by doing a lot of it manually in order to explore error handling; at the end, we’ll show the shorter way. Let’s look at the return type of the function first: Result<String, io::Error>. This means the function is returning a value of the type Result<T, E>, where the generic parameter T has been filled in with the concrete type String and the generic type E has been filled in with the concrete type io::Error.
如果该函数成功运行而没有发生任何问题,调用该函数的代码将收到一个包含 String 的 Ok 值——即该函数从文件中读取的 username。如果该函数遇到任何问题,调用代码将收到一个包含 io::Error 实例的 Err 值,其中包含有关具体问题的更多信息。我们选择 io::Error 作为该函数的返回类型,是因为它恰好是我们在该函数体中调用的两个可能失败操作返回的错误值类型:File::open 函数和 read_to_string 方法。
If this function succeeds without any problems, the code that calls this function will receive an Ok value that holds a String—the username that this function read from the file. If this function encounters any problems, the calling code will receive an Err value that holds an instance of io::Error that contains more information about what the problems were. We chose io::Error as the return type of this function because that happens to be the type of the error value returned from both of the operations we’re calling in this function’s body that might fail: the File::open function and the read_to_string method.
函数体首先调用 File::open 函数。然后,我们使用类似于示例 9-4 中的 match 来处理 Result 值。如果 File::open 成功,模式变量 file 中的文件句柄将成为可变变量 username_file 的值,函数继续执行。在 Err 的情况下,我们不调用 panic!,而是使用 return 关键字从函数中提前返回,并将 File::open 产生的错误值(现在在模式变量 e 中)传回给调用代码,作为该函数的错误值。
The body of the function starts by calling the File::open function. Then, we handle the Result value with a match similar to the match in Listing 9-4. If File::open succeeds, the file handle in the pattern variable file becomes the value in the mutable variable username_file and the function continues. In the Err case, instead of calling panic!, we use the return keyword to return early out of the function entirely and pass the error value from File::open, now in the pattern variable e, back to the calling code as this function’s error value.
因此,如果 username_file 中有一个文件句柄,函数随后在变量 username 中创建一个新的 String,并对 username_file 中的文件句柄调用 read_to_string 方法,将文件的内容读取到 username 中。即使 File::open 成功,read_to_string 方法也可能会失败,因此它也会返回一个 Result。所以,我们需要另一个 match 来处理那个 Result:如果 read_to_string 成功,那么我们的函数就成功了,我们将从文件中读取并现存在 username 中的用户名包装在 Ok 中返回。如果 read_to_string 失败,我们返回错误值,方式与我们在处理 File::open 返回值的 match 中返回错误值的方式相同。但是,我们不需要显式地说 return,因为这是函数中的最后一个表达式。
So, if we have a file handle in username_file, the function then creates a new String in variable username and calls the read_to_string method on the file handle in username_file to read the contents of the file into username. The read_to_string method also returns a Result because it might fail, even though File::open succeeded. So, we need another match to handle that Result: If read_to_string succeeds, then our function has succeeded, and we return the username from the file that’s now in username wrapped in an Ok. If read_to_string fails, we return the error value in the same way that we returned the error value in the match that handled the return value of File::open. However, we don’t need to explicitly say return, because this is the last expression in the function.
调用此代码的代码随后将处理获取包含用户名的 Ok 值或包含 io::Error 的 Err 值的情况。由调用代码来决定如何处理这些值。例如,如果调用代码获得一个 Err 值,它可以调用 panic! 并使程序崩溃,使用默认用户名,或者从文件以外的其他地方查找用户名。我们没有关于调用代码实际尝试做什么的足够信息,因此我们将所有成功或错误信息向上传播,以便其进行适当处理。
The code that calls this code will then handle getting either an Ok value that contains a username or an Err value that contains an io::Error. It’s up to the calling code to decide what to do with those values. If the calling code gets an Err value, it could call panic! and crash the program, use a default username, or look up the username from somewhere other than a file, for example. We don’t have enough information on what the calling code is actually trying to do, so we propagate all the success or error information upward for it to handle appropriately.
这种传播错误的模式在 Rust 中非常普遍,以至于 Rust 提供了问号运算符 ? 来简化此过程。
This pattern of propagating errors is so common in Rust that Rust provides the question mark operator ? to make this easier.
? 运算符的简捷性
The ? Operator Shortcut
示例 9-7 展示了 read_username_from_file 的一个实现,它具有与示例 9-6 相同的功能,但该实现使用了 ? 运算符。
Listing 9-7 shows an implementation of read_username_from_file that has the same functionality as in Listing 9-6, but this implementation uses the ? operator.
#![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)
}
}
放置在 Result 值之后的 ? 被定义为:其工作方式与我们在示例 9-6 中定义用于处理 Result 值的 match 表达式几乎相同。如果 Result 的值是 Ok,则 Ok 内部的值将从该表达式中返回,程序继续运行。如果值是 Err,则该 Err 将从整个函数中返回,就像我们使用了 return 关键字一样,以便错误值传播到调用代码。
The ? placed after a Result value is defined to work in almost the same way as the match expressions that we defined to handle the Result values in Listing 9-6. If the value of the Result is an Ok, the value inside the Ok will get returned from this expression, and the program will continue. If the value is an Err, the Err will be returned from the whole function as if we had used the return keyword so that the error value gets propagated to the calling code.
示例 9-6 中的 match 表达式所做的与 ? 运算符所做的之间存在差异:被调用 ? 运算符的错误值会经过 from 函数,该函数定义在标准库的 From trait 中,用于将值从一种类型转换为另一种类型。当 ? 运算符调用 from 函数时,接收到的错误类型会被转换为当前函数返回类型中定义的错误类型。当一个函数返回一种错误类型来代表函数可能失败的所有方式时,即使各部分可能因为许多不同的原因而失败,这也会非常有用。
There is a difference between what the match expression from Listing 9-6 does and what the ? operator does: Error values that have the ? operator called on them go through the from function, defined in the From trait in the standard library, which is used to convert values from one type into another. When the ? operator calls the from function, the error type received is converted into the error type defined in the return type of the current function. This is useful when a function returns one error type to represent all the ways a function might fail, even if parts might fail for many different reasons.
例如,我们可以将示例 9-7 中的 read_username_from_file 函数更改为返回我们定义的名为 OurError 的自定义错误类型。如果我们还定义了 impl From<io::Error> for OurError 以从 io::Error 构造 OurError 的实例,那么 read_username_from_file 体内的 ? 运算符调用将调用 from 并转换错误类型,而无需向函数添加任何更多代码。
For example, we could change the read_username_from_file function in Listing 9-7 to return a custom error type named OurError that we define. If we also define impl From<io::Error> for OurError to construct an instance of OurError from an io::Error, then the ? operator calls in the body of read_username_from_file will call from and convert the error types without needing to add any more code to the function.
在示例 9-7 的上下文中,File::open 调用末尾的 ? 将把 Ok 内部的值返回给变量 username_file。如果发生错误,? 运算符将从整个函数中提前返回,并向调用代码提供任何 Err 值。同样的情况也适用于 read_to_string 调用末尾的 ?。
In the context of Listing 9-7, the ? at the end of the File::open call will return the value inside an Ok to the variable username_file. If an error occurs, the ? operator will return early out of the whole function and give any Err value to the calling code. The same thing applies to the ? at the end of the read_to_string call.
? 运算符消除了大量样板代码,并使该函数的实现更简单。我们甚至可以通过在 ? 之后立即链接方法调用来进一步缩短此代码,如示例 8-8 所示。
The ? operator eliminates a lot of boilerplate and makes this function’s implementation simpler. We could even shorten this code further by chaining method calls immediately after the ?, as shown in Listing 9-8.
#![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)
}
}
我们将 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 相同;这只是一种不同的、更符合人体工程学的编写方式。
We’ve moved the creation of the new String in username to the beginning of the function; that part hasn’t changed. Instead of creating a variable username_file, we’ve chained the call to read_to_string directly onto the result of File::open("hello.txt")?. We still have a ? at the end of the read_to_string call, and we still return an Ok value containing username when both File::open and read_to_string succeed rather than returning errors. The functionality is again the same as in Listing 9-6 and Listing 9-7; this is just a different, more ergonomic way to write it.
示例 9-9 展示了一种使用 fs::read_to_string 使其变得更短的方法。
Listing 9-9 shows a way to make this even shorter using fs::read_to_string.
#![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 函数,该函数打开文件、创建一个新的 String、读取文件内容、将内容放入该 String 并将其返回。当然,使用 fs::read_to_string 无法让我们有机会解释所有的错误处理,所以我们先用较长的方式完成了它。
Reading a file into a string is a fairly common operation, so the standard library provides the convenient fs::read_to_string function that opens the file, creates a new String, reads the contents of the file, puts the contents into that String, and returns it. Of course, using fs::read_to_string doesn’t give us the opportunity to explain all the error handling, so we did it the longer way first.
哪里可以使用 ? 运算符
Where to Use the ? Operator
? 运算符只能在返回类型与 ? 所使用的值兼容的函数中使用。这是因为 ? 运算符被定义为执行值的提前返回,方式与我们在示例 9-6 中定义的 match 表达式相同。在示例 9-6 中,match 使用的是 Result 值,提前返回分支返回的是 Err(e) 值。函数的返回类型必须是 Result,以便与该 return 兼容。
The ? operator can only be used in functions whose return type is compatible with the value the ? is used on. This is because the ? operator is defined to perform an early return of a value out of the function, in the same manner as the match expression we defined in Listing 9-6. In Listing 9-6, the match was using a Result value, and the early return arm returned an Err(e) value. The return type of the function has to be a Result so that it’s compatible with this return.
在示例 9-10 中,让我们看看如果在返回类型与我们使用 ? 的值类型不兼容的 main 函数中使用 ? 运算符会得到什么错误。
In Listing 9-10, let’s look at the error we’ll get if we use the ? operator in a main function with a return type that is incompatible with the type of the value we use ? on.
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt")?;
}
这段代码打开一个文件,该操作可能会失败。? 运算符跟在 File::open 返回的 Result 值之后,但此 main 函数的返回类型是 (),而不是 Result。当我们编译此代码时,会得到以下错误消息:
This code opens a file, which might fail. The ? operator follows the Result value returned by File::open, but this main function has the return type of (), not Result. When we compile this code, we get the following error message:
$ 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: 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 的类型的函数中使用 ? 运算符。
This error points out that we’re only allowed to use the ? operator in a function that returns Result, Option, or another type that implements FromResidual.
要修复该错误,你有两个选择。一种选择是将函数的返回类型更改为与你使用 ? 运算符的值兼容,前提是你没有阻止这样做的限制。另一种选择是使用 match 或 Result<T, E> 的某种方法,以任何适当的方式处理 Result<T, E>。
To fix the error, you have two choices. One choice is to change the return type of your function to be compatible with the value you’re using the ? operator on as long as you have no restrictions preventing that. The other choice is to use a match or one of the Result<T, E> methods to handle the Result<T, E> in whatever way is appropriate.
错误消息还提到 ? 也可以用于 Option<T> 值。与对 Result 使用 ? 一样,你只能在返回 Option 的函数中对 Option 使用 ?。在 Option<T> 上调用 ? 运算符的行为类似于在 Result<T, E> 上调用它的行为:如果值是 None,则 None 将在此时从函数提前返回。如果值是 Some,则 Some 内部的值就是表达式的结果值,函数继续执行。示例 9-11 展示了一个在给定文本中查找第一行最后一个字符的函数。
The error message also mentioned that ? can be used with Option<T> values as well. As with using ? on Result, you can only use ? on Option in a function that returns an Option. The behavior of the ? operator when called on an Option<T> is similar to its behavior when called on a Result<T, E>: If the value is None, the None will be returned early from the function at that point. If the value is Some, the value inside the Some is the resultant value of the expression, and the function continues. Listing 9-11 has an example of a function that finds the last character of the first line in the given text.
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);
}
该函数返回 Option<char>,因为那里可能有一个字符,但也可能没有。这段代码获取 text 字符串切片参数并对其调用 lines 方法,该方法返回字符串中各行的迭代器。由于此函数想要检查第一行,因此它对迭代器调用 next 以从迭代器获取第一个值。如果 text 是空字符串,则此次 next 调用将返回 None,在这种情况下,我们使用 ? 停止并从 last_char_of_first_line 返回 None。如果 text 不是空字符串,则 next 将返回一个包含 text 第一行字符串切片的 Some 值。
This function returns Option<char> because it’s possible that there is a character there, but it’s also possible that there isn’t. This code takes the text string slice argument and calls the lines method on it, which returns an iterator over the lines in the string. Because this function wants to examine the first line, it calls next on the iterator to get the first value from the iterator. If text is the empty string, this call to next will return None, in which case we use ? to stop and return None from last_char_of_first_line. If text is not the empty string, next will return a Some value containing a string slice of the first line in text.
? 提取字符串切片,我们可以对该字符串切片调用 chars 以获取其字符的迭代器。我们对这第一行的最后一个字符感兴趣,因此我们调用 last 返回迭代器中的最后一项。这是一个 Option,因为第一行可能是空字符串;例如,如果 text 以空行开头,但在其他行有字符,如 "\nhi"。但是,如果第一行有最后一个字符,它将以 Some 变体返回。中间的 ? 运算符为我们提供了一种简洁的方式来表达此逻辑,从而使我们能够用一行代码实现该函数。如果我们不能在 Option 上使用 ? 运算符,我们就不得不使用更多的方法调用或 match 表达式来实现此逻辑。
The ? extracts the string slice, and we can call chars on that string slice to get an iterator of its characters. We’re interested in the last character in this first line, so we call last to return the last item in the iterator. This is an Option because it’s possible that the first line is the empty string; for example, if text starts with a blank line but has characters on other lines, as in "\nhi". However, if there is a last character on the first line, it will be returned in the Some variant. The ? operator in the middle gives us a concise way to express this logic, allowing us to implement the function in one line. If we couldn’t use the ? operator on Option, we’d have to implement this logic using more method calls or a match expression.
请注意,你可以在返回 Result 的函数中对 Result 使用 ? 运算符,也可以在返回 Option 的函数中对 Option 使用 ? 运算符,但不能混用。? 运算符不会自动将 Result 转换为 Option 或反之亦然;在这些情况下,你可以使用 Result 上的 ok 方法或 Option 上的 ok_or 方法等方法来显式进行转换。
Note that you can use the ? operator on a Result in a function that returns Result, and you can use the ? operator on an Option in a function that returns Option, but you can’t mix and match. The ? operator won’t automatically convert a Result to an Option or vice versa; in those cases, you can use methods like the ok method on Result or the ok_or method on Option to do the conversion explicitly.
到目前为止,我们使用的所有 main 函数都返回 ()。main 函数很特别,因为它是可执行程序的入口点和出口点,而且为了让程序行为符合预期,对其返回类型有所限制。
So far, all the main functions we’ve used return (). The main function is special because it’s the entry point and exit point of an executable program, and there are restrictions on what its return type can be for the program to behave as expected.
幸运的是,main 也可以返回 Result<(), E>。示例 9-12 包含了示例 9-10 中的代码,但我们将 main 的返回类型更改为 Result<(), Box<dyn Error>>,并在末尾添加了一个返回值 Ok(())。这段代码现在可以编译了。
Luckily, main can also return a Result<(), E>. Listing 9-12 has the code from Listing 9-10, but we’ve changed the return type of main to be Result<(), Box<dyn Error>> and added a return value Ok(()) to the end. This code will now compile.
use std::error::Error;
use std::fs::File;
fn main() -> Result<(), Box<dyn Error>> {
let greeting_file = File::open("hello.txt")?;
Ok(())
}
Box<dyn Error> 类型是一个 trait 对象,我们将在第 18 章的“使用 trait 对象来抽象不同类型的值”中讨论。目前,你可以将 Box<dyn Error> 理解为“任何类型的错误”。在错误类型为 Box<dyn Error> 的 main 函数中,允许对 Result 值使用 ?,因为它允许提前返回任何 Err 值。尽管此 main 函数体只会返回 std::io::Error 类型的错误,但通过指定 Box<dyn Error>,即使在 main 体内添加了更多返回其他错误的代码,此签名也将继续保持正确。
The Box<dyn Error> type is a trait object, which we’ll talk about in “Using Trait Objects to Abstract over Shared Behavior” in Chapter 18. For now, you can read Box<dyn Error> to mean “any kind of error.” Using ? on a Result value in a main function with the error type Box<dyn Error> is allowed because it allows any Err value to be returned early. Even though the body of this main function will only ever return errors of type std::io::Error, by specifying Box<dyn Error>, this signature will continue to be correct even if more code that returns other errors is added to the body of main.
当 main 函数返回 Result<(), E> 时,如果 main 返回 Ok(()),可执行程序将以 0 值退出;如果 main 返回 Err 值,程序将以非零值退出。用 C 语言编写的可执行程序在退出时返回整数:成功退出的程序返回整数 0,报错的程序返回 0 以外的某个整数。Rust 也从可执行程序返回整数,以符合这一惯例。
When a main function returns a Result<(), E>, the executable will exit with a value of 0 if main returns Ok(()) and will exit with a nonzero value if main returns an Err value. Executables written in C return integers when they exit: Programs that exit successfully return the integer 0, and programs that error return some integer other than 0. Rust also returns integers from executables to be compatible with this convention.
main 函数可以返回任何实现了 std::process::Termination trait 的类型,该 trait 包含一个返回 ExitCode 的 report 函数。有关为自己的类型实现 Termination trait 的更多信息,请查阅标准库文档。
The main function may return any types that implement the std::process::Termination trait, which contains a function report that returns an ExitCode. Consult the standard library documentation for more information on implementing the Termination trait for your own types.
现在我们已经讨论了调用 panic! 或返回 Result 的细节,让我们回到如何决定在哪些情况下使用哪种方法更合适的话题。
Now that we’ve discussed the details of calling panic! or returning Result, let’s return to the topic of how to decide which is appropriate to use in which cases.