可恢复的错误Result

大多数错误没有严重到需要程序完全停止的程度。 有时,当函数失败时,这是由于您可以轻松解释的原因 并做出回应。例如,如果您尝试打开文件,但该作失败 由于该文件不存在,因此您可能希望创建该文件,而不是 终止进程。

召回自“处理潜在故障Result在第 2 章中,Resultenum 定义为具有两个 变种OkErr如下:

#![allow(unused)]
fn main() {
enum Result<T, E> {
    Ok(T),
    Err(E),
}
}

TE是泛型类型参数:我们将在 More detail 在第 10 章中。您现在需要了解的是T代表 在 Success case 中将返回的值的类型Okvariant 和E表示将在 failure 情况下的Err变体。因为Result具有以下泛型 参数,我们可以使用Resulttype 及其上定义的函数 在许多不同的情况下,我们想要 Success Value 和 Error 值 return 可能有所不同。

让我们调用一个函数,该函数返回一个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用于 error 值为std::io::Error.此返回类型表示对File::open可能会成功并返回一个我们可以从 或 写入。函数调用也可能失败:例如,文件可能不会 存在,或者我们可能没有访问该文件的权限。这File::openfunction 需要有办法告诉我们它是成功还是失败,而在 同时给我们 File Handle 或 Error 信息。这 信息正是Resultenum conveys 的 intent 中。

在以下情况下File::opensucceeds,则变量greeting_file_result将是Ok,其中包含一个文件句柄。 在失败的情况下,中的greeting_file_result将是一个 实例Err,其中包含有关 发生。

我们需要添加到示例 9-3 中的代码中,以根据 在值File::open返回。示例 9-4 显示了一种处理Result使用基本工具match表达式,我们在 第 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:使用matchexpression 来处理Result可能返回的变体

请注意,与Optionenum 中,Resultenum 及其变体已被 引入范围,因此我们不需要指定Result::OkErr变体match武器。

当结果为Ok,此代码将返回内部的file值 out of 这Okvariant 中,然后将该文件 handle 值分配给变量greeting_file.在match,我们可以使用文件句柄进行读取或 写作。

的另一只臂match处理我们获取Err值来自File::open.在此示例中,我们选择调用panic!宏。如果 当前目录中没有名为 hello.txt 的文件,我们运行此 code 中,我们将看到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 中的代码将panic!无论为什么File::open失败。 但是,我们希望针对不同的失败原因采取不同的作。如果File::open失败,因为文件不存在,我们想要创建该文件 并返回新文件的句柄。如果File::open任何其他失败 原因(例如,因为我们没有打开文件的权限),我们仍然 希望代码panic!就像示例 9-4 中所做的那样。为此,我们 添加内部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返回值Errvariant 为io::Error,这是标准库提供的 struct。这个结构体 具有kind我们可以调用io::ErrorKind价值。枚举io::ErrorKind由标准库提供,并具有变体 表示io操作。我们想要使用的变体是ErrorKind::NotFound,它指示 我们尝试打开的文件尚不存在。所以我们匹配greeting_file_result,但我们也有error.kind().

我们在内匹配中要检查的条件是,值是否返回 由error.kind()NotFound变体ErrorKindenum 中。如果是, 我们尝试使用File::create.但是,由于File::create也可能失败,我们需要在内部有第二个臂match表达。当 file 无法创建,则会打印不同的错误消息。的第二个臂 外部match保持不变,因此程序会因 缺少文件错误。

使用的替代方案matchResult<T, E>

那可是很多match!这match表达式非常有用,但也非常 很原始。在第 13 章中,您将了解使用的闭包 在Result<T, E>.这些方法可以更多 比使用match处理时Result<T, E>值。

例如,这是另一种编写与 清单 中所示相同的逻辑的方法 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表达式。

Error 时 panic 的快捷方式:unwrapexpect

match效果很好,但可能有点冗长,而且并不总是如此 很好地传达意图。这Result<T, E>type 具有许多帮助程序方法 定义以执行各种更具体的任务。这unwrapmethod 是 shortcut 方法的实现方式与match表达式 示例 9-4.如果Resultvalue 是Ok变体unwrap将返回 值Ok.如果ResultErr变体unwrap将 调用panic!macro 的 Macro 来描述。下面是一个unwrap实际作:

文件名: src/main.rs

use std::fs::File;

fn main() {
    let greeting_file = File::open("hello.txt").unwrap();
}

如果我们在没有 hello.txt 文件的情况下运行此代码,我们将看到来自 这panic!调用该unwrapmethod 使:

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" }

同样,expectmethod 让我们也选择panic!错误信息。 用expect而不是unwrap提供良好的错误消息可以传达 你的意图,并使追踪恐慌的来源更容易。的语法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");
}

我们使用expectunwrap:返回文件句柄或调用 这panic!宏。使用的错误消息expect在它对panic!将是我们传递给expect,而不是默认的panic!消息unwrap使用。这是它的样子:

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" }

在生产质量代码中,大多数 Rustacean 选择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>.这意味着该函数正在返回 value 类型的Result<T, E>,其中泛型参数T已经 填充 具体类型String和泛型类型E已经 填充 具体类型io::Error.

如果此函数成功且没有任何问题,则调用此 函数将接收一个Ok值,其中包含String- 该username那 此函数从文件中读取。如果此函数遇到任何问题,则 调用代码将收到一个Err值,其中包含io::Error其中包含有关问题所在的更多信息。我们选择了io::Error作为此函数的返回类型,因为那恰好是 type 我们从我们调用的两个作返回的错误值 此函数的主体可能会失败:File::open函数和read_to_string方法。

函数的主体首先调用File::open功能。然后我们 处理Result值替换为match类似于match在示例 9-4 中。 如果File::opensucceeds,则 pattern 变量file成为可变变量中的值username_file和函数 继续。在Errcase 而不是调用panic!,我们使用returnkeyword 完全从函数中提前返回并传递 error 值 从File::open,现在位于 pattern 变量e,返回到调用代码 此函数的 error 值。

因此,如果我们在username_file,该函数会创建一个 新增功能Stringin 变量username并调用read_to_stringmethod 开启 文件中的句柄username_file将文件内容读入username.这read_to_stringmethod 还会返回一个Result因为它 可能会失败,即使File::open成功。所以我们需要另一个match自 处理那个Result:如果read_to_stringsucceeds,那么我们的函数就有 succeeded,然后我们从现在位于username包装在Ok.如果read_to_string失败,则返回 与我们在match处理了 返回值File::open.但是,我们不需要明确地说return,因为这是函数中的最后一个表达式。

然后,调用此代码的代码将处理获取Ok价值 ,其中包含 username 或Err值,其中包含io::Error.它 由调用代码决定如何处理这些值。如果调用 code 获取一个Errvalue 中,它可以调用panic!并崩溃程序,请使用 default username 的 NAME 中查找 USERNAME 的 ID 或 1 个应用程序 例。我们没有足够的信息来了解调用代码的实际含义 尝试执行作,因此我们将 它要适当地处理。

这种传播错误的模式在 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:将错误返回给 调用代码?算子

?放置在Resultvalue 的定义方式几乎相同 作为match表达式来处理Result列表中的值 9-6. 如果Result是一个Ok,则Ok将 get 从此表达式返回,程序将继续。如果值 是一个ErrErr将从整个函数返回,就好像我们有 使用了return关键字,以便将错误值传播到调用 法典。

match示例 9-6 中的 expression 执行 以及?运算符执行:具有?运算符调用 在他们身上,遍历from函数,在Fromtrait 中的 标准库,用于将值从一种类型转换为另一种类型。 当?运算符调用from函数,则收到的错误类型为 转换为当前 功能。当函数返回一个错误类型来表示 函数可能失败的所有方式,即使部分可能会因许多不同的原因而失败 原因。

例如,我们可以更改read_username_from_file列表中的函数 9-7 返回一个名为OurError我们定义的。如果我们还 定义impl From<io::Error> for OurError要构造OurErrorio::Error,则?operator 调用read_username_from_file将调用from并转换没有 需要向函数添加更多代码。

在示例 9-7 的上下文中,?File::opencall 将 返回Ok添加到变量username_file.如果出现错误 发生时,该?operator 将在整个函数中 early 返回并给出 任何Err值添加到调用代码中。同样的事情也适用于?在 结束read_to_string叫。

?operator 消除了大量的样板代码,并使该函数的 实现更简单。我们甚至可以通过链接 方法调用?,如示例 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:在?算子

我们已将新的Stringusername到 函数;这部分没有改变。而不是创建变量username_file,我们已将调用链接到read_to_string直接到 的结果File::open("hello.txt")?.我们仍然有一个?read_to_string调用,我们仍然返回一个Ok值包含username当两者File::openread_to_string成功而不是返回 错误。功能与示例 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函数打开 file 中,创建一个新的String、读取文件的内容、放置内容 进入那个String,然后返回它。当然,使用fs::read_to_string没有给我们机会解释所有的错误处理,所以我们这样做了 长路先。

其中,?可以使用 Operator

?operator 只能在返回类型兼容的函数中使用 值为?用于。这是因为?运算符 以相同的方式从函数中提前返回值 作为match表达式,我们在示例 9-6 中定义。在示例 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函数的 Json 函数不会编译。()

此代码将打开一个文件,这可能会失败。这?运算符遵循Result值返回者File::open,但是这个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>: 如果值为NoneNone将提前从 那个点。如果值为Some,则Some是 resultant 值,并且函数继续。示例 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>因为可能存在 字符,但也有可能没有。此代码采用textstring slice 参数并调用lines方法,该 API 将返回 字符串中各行的迭代器。因为这个函数想要 检查第一行,它会调用next获取第一个值 从迭代器。如果text是空字符串,则对next将 返回None,在这种情况下,我们使用?停止并返回Nonelast_char_of_first_line.如果text不是空字符串,next将 返回一个Some值,其中包含 中第一行的字符串切片text.

?提取字符串 slice,我们可以调用chars在那个字符串切片上 获取其字符的迭代器。我们对 中的最后一个字符感兴趣 这第一行,所以我们调用last返回迭代器中的最后一项。 这是一个Option因为第一行可能是空的 字符串;例如,如果text以空行开头,但有 CHARACTERS ON 其他行,如"\nhi".但是,如果第一个字符上有最后一个字符 行中,它将在Some变体。这?运算符在中间 为我们提供了一种简洁的方式来表达这个逻辑,允许我们实现 函数。如果我们无法使用?运算符 onOption,我们会 必须使用更多方法调用或match表达。

请注意,您可以使用?运算符Result在返回Result,您可以使用?运算符Option在函数中, 返回Option,但您不能混合搭配。这?运算符不会 自动转换Result更改为Option反之亦然;在这些情况下, 您可以使用诸如okmethod 开启Resultok_ormethod 开启Option以显式执行转换。

到目前为止,所有main我们使用的函数返回 .这()main函数为 special 的,因为它是可执行程序的入口点和出口点, 并且程序可以返回的 type 是有限制的 按预期作。

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>允许使用?运算符 onResult值。

Box<dyn Error>type 是一个 trait 对象,我们将在 “使用允许不同值的 trait 对象 ” 中讨论 Types“部分。现在,您可以 读Box<dyn Error>的意思是 “任何类型的错误”。用?Result值在main错误类型为Box<dyn Error>允许 因为它允许任何Err值要提前返回。即使 这mainfunction 只会返回std::io::Error由 指定Box<dyn Error>,则此签名将继续正确,即使 更多返回其他错误的代码被添加到main.

main函数返回一个Result<(), E>,可执行文件将退出并显示 值为0如果main返回Ok(()),如果main返回一个Err价值。用 C 语言编写的可执行文件在以下情况下返回整数 they exit:成功退出的程序返回整数0和程序 该错误返回除0.Rust 还从 可执行文件以与此约定兼容。

mainfunction 可以返回任何实现std::process::Termination特性,其中包含 函数report返回一个ExitCode.查阅标准库 documentation 有关实施Termination的 trait 您自己的类型。

现在我们已经讨论了调用panic!或返回Result, 让我们回到如何决定哪个适合在哪个中使用 例。

本文档由官方文档翻译而来,如有差异请以官方英文文档(https://doc.rust-lang.org/)为准