简介
欢迎来到「使用 Result
处理可恢复错误」。本实验是 《Rust 程序设计语言》 的一部分。你可以在 LabEx 中练习 Rust 技能。
在本实验中,我们将学习如何使用 Rust 中的 Result
枚举来处理可恢复错误,这使我们能够在不终止程序的情况下解释和响应错误。
This tutorial is from open-source community. Access the source code
💡 本教程由 AI 辅助翻译自英文原版。如需查看原文,您可以 切换至英文原版
欢迎来到「使用 Result
处理可恢复错误」。本实验是 《Rust 程序设计语言》 的一部分。你可以在 LabEx 中练习 Rust 技能。
在本实验中,我们将学习如何使用 Rust 中的 Result
枚举来处理可恢复错误,这使我们能够在不终止程序的情况下解释和响应错误。
Result
处理可恢复错误大多数错误并不严重到需要程序完全停止。有时,当一个函数失败时,原因是你可以轻松解释和应对的。例如,如果你尝试打开一个文件,但由于文件不存在而导致操作失败,你可能想要创建该文件,而不是终止进程。
回顾「使用 Result
处理潜在失败」,Result
枚举被定义为有两个变体,Ok
和 Err
,如下所示:
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 展示了一种使用基本工具 match
表达式来处理 Result
的方法,我们在第 6 章中讨论过这个表达式。
文件名: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!
宏中看到以下输出:
thread'main' panicked at 'Problem opening the file: Os { code:
2, kind: NotFound, message: "No such file or directory" }',
src/main.rs:8:23
像往常一样,这个输出准确地告诉我们哪里出了问题。
无论 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!
。
match
处理 Result<T, E>
的方法这么多 match
表达式啊!match
表达式非常有用,但也是一种非常原始的方式。在第 13 章中,你将学习闭包,它们会与许多在 Result<T, E>
上定义的方法一起使用。当你在代码中处理 Result<T, E>
值时,这些方法可能比使用 match
更简洁。
例如,这是另一种编写与清单 9-5 中相同逻辑的方式,这次使用闭包和 unwrap_or_else
方法:
// src/main.rs
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 '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
的语法如下:
文件名: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 'hello.txt should be included in this project: Os {
code: 2, kind: NotFound, message: "No such file or directory" }',
src/main.rs:5:10
在生产质量的代码中,大多数 Rust 开发者会选择 expect
而不是 unwrap
,并给出更多关于为什么该操作预期总是会成功的上下文信息。这样,如果你的假设被证明是错误的,你在调试时就有更多信息可供使用。
当一个函数的实现调用了可能失败的操作时,你可以将错误返回给调用代码,而不是在函数内部处理错误,这样调用代码就能决定如何处理。这被称为传播错误,它能给调用代码更多控制权,因为在调用代码的上下文中,可能有更多信息或逻辑来决定如何处理错误,而不仅仅是你在自己代码中所拥有的。
例如,清单 9-6 展示了一个从文件中读取用户名的函数。如果文件不存在或无法读取,这个函数会将这些错误返回给调用该函数的代码。
文件名:src/main.rs
use std::fs::File;
use std::io::{self, Read};
1 fn read_username_from_file() -> Result<String, io::Error> {
2 let username_file_result = File::open("hello.txt");
3 let mut username_file = match username_file_result {
4 Ok(file) => file,
5 Err(e) => return Err(e),
};
6 let mut username = String::new();
7 match username_file.read_to_string(&mut username) {
8 Ok(_) => Ok(username),
9 Err(e) => Err(e),
}
}
清单 9-6:使用 match
将错误返回给调用代码的函数
这个函数可以用更简短的方式编写,但为了探索错误处理,我们先手动完成很多步骤;最后,我们会展示更简短的方式。让我们先看看函数的返回类型:Result<String, io::Error>
[1]。这意味着函数返回一个 Result<T, E>
类型的值,其中泛型参数 T
被具体类型 String
填充,泛型类型 E
被具体类型 io::Error
填充。
如果这个函数成功且没有任何问题,调用这个函数的代码将收到一个包含 String
的 Ok
值 —— 即这个函数从文件中读取的 username
[8]。如果这个函数遇到任何问题,调用代码将收到一个包含 io::Error
实例的 Err
值,该实例包含有关问题的更多信息。我们选择 io::Error
作为这个函数的返回类型,是因为这恰好是这个函数体中调用的两个可能失败的操作返回的错误值的类型:File::open
函数 [2] 和 read_to_string
方法 [7]。
函数体首先调用 File::open
函数 [2]。然后我们使用类似于清单 9-4 中的 match
来处理 Result
值。如果 File::open
成功,模式变量 file
中的文件句柄 [4] 成为可变变量 username_file
中的值 [3],函数继续执行。在 Err
情况下,我们不调用 panic!
,而是使用 return
关键字提前完全退出函数,并将 File::open
的错误值(现在在模式变量 e
中)作为这个函数的错误值返回给调用代码 [5]。
所以,如果 username_file
中有一个文件句柄,函数然后在变量 username
中创建一个新的 String
[6],并对 username_file
中的文件句柄调用 read_to_string
方法,将文件内容读入 username
[7]。read_to_string
方法也返回一个 Result
,因为它可能失败,即使 File::open
成功了。所以我们需要另一个 match
来处理那个 Result
:如果 read_to_string
成功,那么我们的函数就成功了,我们返回文件中的用户名(现在在 username
中),并包装在一个 Ok
中。如果 read_to_string
失败,我们以与处理 File::open
返回值的 match
中返回错误值相同的方式返回错误值。不过,我们不需要明确写 return
,因为这是函数中的最后一个表达式 [9]。
调用这段代码的代码然后将处理获取到的包含用户名的 Ok
值或包含 io::Error
的 Err
值。由调用代码决定如何处理这些值。如果调用代码得到一个 Err
值,它可以调用 panic!
并使程序崩溃,使用默认用户名,或者从文件以外的其他地方查找用户名,例如。我们没有足够的信息了解调用代码实际要做什么,所以我们向上传播所有的成功或错误信息,以便它能适当地处理。
这种传播错误的模式在 Rust 中非常常见,以至于 Rust 提供了问号运算符 ?
来简化这个过程。
清单 9-7 展示了 read_username_from_file
的另一种实现,它与清单 9-6 具有相同的功能,但此实现使用了 ?
运算符。
文件名:src/main.rs
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
trait 定义的 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 所示。
文件名:src/main.rs
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
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
。当我们编译这段代码时,会得到以下错误信息:
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() {
4 | | let greeting_file = File::open("hello.txt")?;
| | ^ cannot use the `?`
operator in a function that returns `()`
5 | | }
| |_- this function should return `Result` or `Option` to accept `?`
|
= help: the trait `FromResidual<Result<Infallible, std::io::Error>>` is not
implemented for `()`
这个错误指出我们只被允许在返回 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()
}
清单 9-11:对 Option<T>
值使用 ?
运算符
这个函数返回 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(())
。这段代码现在将编译通过。
文件名: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 对象”中讨论它。目前,你可以将 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
的细节,现在让我们回到如何决定在哪些情况下使用哪种方式更合适的话题。
恭喜你!你已经完成了“使用 Result 处理可恢复错误”实验。你可以在 LabEx 中练习更多实验来提升你的技能。