结果的可恢复错误

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

回想一下第 2 章的 “Handling Potential Failure with Result 中,枚举被定义为具有两个 variants 和 ,如下所示:ResultOkErr

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

和 是泛型类型参数:我们将在 detail 在第 10 章中。您现在需要知道的是,它代表 在变体中的成功案例中将返回的值的类型,并表示将在 failure 大小写。因为有这些泛型 parameters 中,我们可以使用 type 和 在许多不同的情况下,我们想要 Success Value 和 Error 值 return 可能有所不同。TETOkEErrResultResult

让我们调用一个返回值的函数,因为该函数可以 失败。在示例 9-3 中,我们尝试打开一个文件。Result

文件名: src/main.rs

use std::fs::File;

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

示例 9-3:打开一个文件

的返回类型是 .泛型参数已由 implementation of 填充,其类型为 success 值,即文件句柄。中使用的类型 错误值为 。此返回类型意味着对 的调用可能会成功,并返回我们可以从 或 写入。函数调用也可能失败:例如,文件可能不会 存在,或者我们可能没有访问该文件的权限。该函数需要有一种方法来告诉我们它是成功还是失败,并且在 同时给我们 File Handle 或 Error 信息。这 information 正是 enum 所传达的。File::openResult<T, E>TFile::openstd::fs::FileEstd::io::ErrorFile::openFile::openResult

如果成功,则变量中的值将是包含文件句柄的实例。 在失败的情况下,值 in 将为 实例中包含有关 发生。File::opengreeting_file_resultOkgreeting_file_resultErr

我们需要添加到示例 9-3 中的代码中,以根据 的值返回。示例 9-4 展示了一种使用基本工具的方法,我们在 第 6 章.File::openResultmatch

文件名: 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 变体

请注意,与枚举一样,枚举及其变体也是 引入范围,因此我们不需要在 ARMS 中的 AND 变体之前指定。OptionResultResult::OkErrmatch

当结果为 时,此代码将返回 变体,然后将该文件 handle 值分配给变量 .在 之后,我们可以使用文件句柄读取或 写作。OkfileOkgreeting_filematch

另一个分支处理我们从 获取值的情况。在此示例中,我们选择调用宏。如果 当前目录中没有名为 hello.txt 的文件,我们运行此 code 中,我们将看到宏的以下输出:matchErrFile::openpanic!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 中的代码无论为什么失败。 但是,我们希望针对不同的失败原因采取不同的作。如果因为文件不存在而失败,我们想创建该文件 并返回新文件的句柄。如果任何其他 原因(例如,因为我们没有打开文件的权限),我们仍然 希望代码像示例 9-4 中那样。为此,我们 添加一个内部表达式,如示例 9-5 所示。panic!File::openFile::openFile::openpanic!match

文件名: 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:处理 中的不同类型的错误 不同的方式

在 variant 中返回的值的类型是 ,它是标准库提供的 struct 。这个结构体 有一个方法,我们可以调用它来获取一个值。枚举由标准库提供,并具有变体 表示作可能导致的不同种类的错误。我们要使用的变体是 ,它表示 我们尝试打开的文件尚不存在。所以我们在 上匹配,但我们在 上也有一个内部匹配。File::openErrio::Errorkindio::ErrorKindio::ErrorKindioErrorKind::NotFoundgreeting_file_resulterror.kind()

我们在内匹配中要检查的条件是,值是否返回 by 是枚举的变体。如果是, 我们尝试使用 .但是,由于也可能失败,因此我们需要在 inner 表达式中使用第二个分支。当 file 无法创建,则会打印不同的错误消息。的第二个臂 outer 保持不变,因此程序会对 缺少文件错误。error.kind()NotFoundErrorKindFile::createFile::creatematchmatch

matchResult<T, E> 一起使用的替代方法

好多啊!这个表达式非常有用,但也非常 很原始。在第 13 章中,您将了解使用的闭包 在 上定义了许多方法。这些方法可以更多 比在代码中处理值时使用更简洁。matchmatchResult<T, E>matchResult<T, E>

例如,这是另一种编写与 清单 中所示相同的逻辑的方法 9-5,此时使用 closure 和方法: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 相同,但它不包含 any 表达式,并且更易于阅读。回到这个例子 阅读完第 13 章后,在 标准库文档。更多这些方法可以清理巨大的 嵌套表达式。matchunwrap_or_elsematch

Panic on Error 的快捷方式:unwrapexpect

使用效果很好,但它可能有点冗长,而且并不总是如此 很好地传达意图。该类型具有许多帮助程序方法 定义以执行各种更具体的任务。该方法是一个 shortcut 方法的实现方式,就像我们在 示例 9-4.如果值是 variant,将返回 值 内 .如果 是变体,则 为我们调用 Macro。下面是一个实际作的示例:matchResult<T, E>unwrapmatchResultOkunwrapOkResultErrunwrappanic!unwrap

文件名: src/main.rs

use std::fs::File;

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

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

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

同样,该方法也允许我们选择错误消息。 使用而不是提供良好的错误消息可以传达 你的意图,并使追踪恐慌的来源更容易。的语法如下所示:expectpanic!expectunwrapexpect

文件名: 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");
}

我们以与 : 相同的方式返回文件句柄或调用 宏。在其调用 to 中使用的错误消息 by 将是我们传递给 的参数,而不是 use 的默认消息。这是它的样子:expectunwrappanic!expectpanic!expectpanic!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 选择 instead 而不是 and 给出了更多上下文来说明为什么作应该总是 成功。这样,如果你的假设被证明是错误的,你就会有更多的 调试中使用的信息。expectunwrap

传播错误

当函数的实现调用可能会失败的内容时,而不是 在函数本身内处理错误时,可以将错误返回到 调用代码,以便它可以决定要做什么。这称为传播错误,并为调用代码提供更多控制权,其中可能有更多 指示应如何处理错误的信息或逻辑,而不是 您在代码的上下文中可用。

例如,示例 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 调用代码

这个函数可以用更短的方式编写,但我们要从 手动执行大量作以探索错误处理;最后, 我们将展示更短的方法。让我们看看函数的返回类型 第一:。这意味着该函数正在返回 type 的值,其中泛型参数已 填充了具体类型,并且泛型类型已被 填充了具体类型 .Result<String, io::Error>Result<T, E>TStringEio::Error

如果此函数成功且没有任何问题,则调用此 函数将接收一个值,该值包含一个 — 该 此函数从文件中读取。如果此函数遇到任何问题,则 调用 Code 将接收一个值,该值包含一个实例,其中包含有关问题所在的更多信息。我们选择作为此函数的返回类型,因为它恰好是 type 我们从我们调用的两个作返回的错误值 此函数的主体可能会失败:函数和方法。OkStringusernameErrio::Errorio::ErrorFile::openread_to_string

函数的主体从调用函数开始。然后我们 handle 值,类似于示例 9-4 中的。 如果成功,则 pattern 变量中的文件句柄将成为 mutable 变量中的值,函数 继续。在这种情况下,我们不是调用 ,而是使用关键字来完全提前返回函数并传递 error 值 from ,现在在 pattern 变量中,返回到调用代码 此函数的 error 值。File::openResultmatchmatchFile::openfileusername_fileErrpanic!returnFile::opene

因此,如果我们在 中有一个文件句柄,该函数就会创建一个 new 在变量中调用 文件句柄 in 将文件内容读入 .该方法还返回一个 ,因为它 即使成功,也可能失败。所以我们需要另一个 处理 : 如果成功,那么我们的函数就有 成功,然后我们从现在包装在 .如果失败,则返回 与我们在处理 返回值 。但是,我们不需要显式地说 ,因为这是函数中的最后一个表达式。username_fileStringusernameread_to_stringusername_fileusernameread_to_stringResultFile::openmatchResultread_to_stringusernameOkread_to_stringmatchFile::openreturn

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

这种传播错误的模式在 Rust 中非常常见,以至于 Rust 提供了 问号运算符来简化此作。?

传播错误的快捷方式:算子

示例 9-7 显示了一个具有 功能与示例 9-6 中的相同,但此实现使用 operator。read_username_from_file?

文件名: 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:将错误返回给 使用 运算符调用代码

放置在值之后的 is defined 以几乎相同的方式工作 作为表达式来处理 清单 中的值 9-6. 如果 the 的值是 an ,则 will 中的值 get 从此表达式返回,程序将继续。如果值 是一个 ,它将从整个函数中返回,就好像我们有 使用了关键字,以便将错误值传播到调用 法典。?ResultmatchResultResultOkOkErrErrreturn

示例 9-6 中的表达式所做的事情是不同的 以及运算符的作用:运算符名为 遍历函数,该函数在 标准库,用于将值从一种类型转换为另一种类型。 当作员调用函数时,收到的错误类型为 转换为当前 功能。当函数返回一个错误类型来表示 函数可能失败的所有方式,即使部分可能会因许多不同的原因而失败 原因。match??fromFrom?from

例如,我们可以更改 清单 中的函数 9-7 返回一个名为我们定义的自定义错误类型。如果我们还 define 构造 的实例,则 body 中的运算符调用将调用并转换没有 需要向函数添加更多代码。read_username_from_fileOurErrorimpl From<io::Error> for OurErrorOurErrorio::Error?read_username_from_filefrom

在示例 9-7 的上下文中,调用结束时的 the 将 将 an 中的值返回给变量 。如果出现错误 发生时,运算符会提前返回整个函数,并给出 任何值。同样的事情也适用于 at 通话结束。?File::openOkusername_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:在 运算符之后链接方法调用

我们已将 new in 的创建移至 函数;这部分没有改变。我们没有创建变量,而是将 to 的调用直接链接到 的结果。我们在调用的末尾仍然有 a,并且我们仍然返回一个包含 when 和 succeed 的值,而不是返回 错误。功能与示例 9-6 和示例 9-7 中的相同; 这只是一种不同的、更符合人体工程学的编写方式。Stringusernameusername_fileread_to_stringFile::open("hello.txt")??read_to_stringOkusernameFile::openread_to_string

示例 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 代替 打开然后读取文件

将文件读入字符串是一个相当常见的作,因此标准的 库提供了方便的函数,打开 file, create a new , 读取文件的内容, 放置内容 放入该 ,并返回它。当然,using 并没有给我们解释所有错误处理的机会,所以我们这样做了 长路先。fs::read_to_stringStringStringfs::read_to_string

其中 可以使用 Operator

运算符只能在返回类型兼容的函数中使用 替换为 the is used on 的值。这是因为运算符是定义的 以相同的方式从函数中提前返回值 作为我们在示例 9-6 中定义的表达式。在示例 9-6 中,使用的是 value,而 early return arm 返回了一个值。函数的返回类型必须是 ,以便 它与这个兼容。???matchmatchResultErr(e)Resultreturn

在示例 9-10 中,让我们看看如果我们使用 operator 在返回类型与 我们使用的值。?main?

文件名: src/main.rs

use std::fs::File;

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

示例 9-10:尝试在返回 ()main 函数中使用 不会编译。

此代码将打开一个文件,这可能会失败。运算符遵循 返回的值,但此函数的返回类型为 ,而不是 。当我们编译此代码时,我们收到以下错误 消息:?ResultFile::openmain()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

这个错误指出我们只允许在 返回 、 、 或实现 .?ResultOptionFromResidual

要修复此错误,您有两种选择。一种选择是更改返回类型 的函数,以与您使用的值兼容运算符 只要你没有限制阻止它。另一种选择是 使用 A 或其中一种方法以任何适当的方式处理 。?matchResult<T, E>Result<T, E>

错误消息还提到了可以与值一起使用 也。与使用 on 一样,您只能在 函数返回一个 .运算符在调用时的行为 on 类似于它在 : 如果值为 ,则 将提前从位于 那个点。如果值为 ,则 中的值为 resultant 值,并且函数继续。示例 9-11 有 一个函数示例,该函数在 给定的文本。?Option<T>?Result?OptionOption?Option<T>Result<T, E>NoneNoneSomeSome

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> 值上使用 运算符

此函数返回是因为可能存在 字符,但也有可能没有。此代码采用字符串 slice 参数并对其调用方法,该方法返回 字符串中各行的迭代器。因为这个函数想要 检查第一行,它调用 Iterator 来获取第一个值 从迭代器。如果 是空字符串,则对 will return ,在这种情况下,我们使用 stop 和 return from 。如果 不是空字符串,则 返回一个值,其中包含 中第一行的字符串 slice。Option<char>textlinesnexttextnextNone?Nonelast_char_of_first_linetextnextSometext

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

请注意,您可以在返回 的函数中的 上使用运算符,也可以在 in 函数上使用运算符,该函数 返回 ,但不能混合和匹配。作员不会 自动将 A 转换为 AN,反之亦然;在这些情况下, 您可以使用 Method on 或 Method On 等方法来显式执行转换。?ResultResult?OptionOption?ResultOptionokResultok_orOption

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

幸运的是,也可以返回一个 .示例 9-12 的代码 从示例 9-10 开始,但我们把 的返回类型改成了 be,并在末尾添加了一个返回值。这 代码现在将编译。mainResult<(), E>mainResult<(), 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 值上使用 运算符。

类型是一个 trait 对象,我们将在 “使用允许不同值的 trait 对象 ” 中讨论 Types“部分。现在,您可以 读作 “任何类型的错误”。允许在具有 error 类型的函数中使用值 ,因为它允许提前返回任何值。即使 此函数将仅返回 , 由 指定 ,即使 返回其他错误的更多代码将添加到 的正文中。Box<dyn Error>Box<dyn Error>?ResultmainBox<dyn Error>Errmainstd::io::ErrorBox<dyn Error>main

当函数返回 时,可执行文件将退出,并显示 值 if 返回,如果返回值,则以非零值退出。用 C 语言编写的可执行文件在以下情况下返回整数 它们 exit:成功退出的程序返回 integer ,而 programs 该错误返回除 以外的某个整数。Rust 还从 可执行文件以与此约定兼容。mainResult<(), E>0mainOk(())mainErr00

该函数可以返回实现 std::p rocess::Termination trait 的任何类型的 intent,其中包含 一个返回 .查阅标准库 documentation 中有关为 您自己的类型。mainreportExitCodeTermination

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

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