结果的可恢复错误


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


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

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


TE 是泛型类型参数:我们将在第 10 章更详细地讨论泛型。您现在需要知道的是,T 表示在 Ok variant 的 Variant 表示在 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 function 需要有办法告诉我们它是成功还是失败,而在 同时给我们 File Handle 或 Error 信息。这 information 正是 Result 枚举所传达的。


File::open 成功的情况下,变量 greeting_file_result 将是包含文件句柄的 Ok 实例。在失败的情况下,greeting_file_result 中的值将是 Err 的实例,其中包含有关所发生的错误类型的更多信息。


我们需要添加到示例 9-3 中的代码中,以根据 File::open 返回的值采取不同的作。示例 9-4 显示了一种处理 结果使用基本工具,我们在第 6 章中讨论的 match 表达式。


文件名: 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 表达式处理 可能返回的结果变量


请注意,与 Option 枚举一样,Result 枚举及其变体已被 prelude 引入范围,因此我们不需要指定 Result::match 臂中的 OkErr 变体之前。


当结果为 Ok 时,此代码将从 Ok 变体中返回内部文件值,然后将该文件句柄值分配给变量 greeting_file匹配后,我们可以使用 file handle 进行读取或写入。


match 的另一条臂处理我们从中获取 Err 值的情况 File::open 中。在此示例中,我们选择调用 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 中的代码会 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::openErr 变体中返回的值的类型是 io::Error 的 Error,这是标准库提供的一个结构体。这个结构体有一个方法类型,我们可以调用它来获取 io::ErrorKind 值。枚举 io::ErrorKind 由标准库提供,其变体表示 io 可能导致的不同种类的错误 操作。我们想要使用的变体是 ErrorKind::NotFound,它指示 我们尝试打开的文件尚不存在。所以我们匹配 greeting_file_result,但我们在 error.kind() 上也有一个内部匹配。


我们要在内部匹配中检查的条件是 error.kind() 返回的值是否是 ErrorKind 枚举的 NotFound 变体。如果是,我们尝试使用 File::create 创建文件。但是,由于 File::create 也可能失败,我们需要内部 match 表达式中的第二个分支。当无法创建文件时,将打印不同的错误消息。外部匹配项的第二个分支保持不变,因此程序会因除 missing file 错误之外的任何错误出现 panic。


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


那是很多匹配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 表达式。


Panic on Error 的快捷方式:unwrapexpect


使用 match 效果很好,但它可能有点冗长,并且并不总是能很好地传达意图。Result<T, E> 类型定义了许多帮助程序方法,用于执行各种更具体的任务。unwrap 方法是一种快捷方法,实现方式就像我们在示例 9-4 中编写的 match 表达式一样。如果 Result 值是 Ok 变体,则 unwrap 将返回 Ok.如果 ResultErr 变体,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 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 如下所示:


文件名: 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 的参数,而不是默认的 恐慌!消息。这是它的样子:

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>。这意味着该函数返回 Result<T, E> 类型的值,其中泛型参数 T 已填充为具体类型 String,泛型类型 E 已填充为具体类型 io::Error


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


函数的主体首先调用 File::open 函数。然后,我们使用类似于示例 9-4 中的 matchmatch 来处理 Result 值。如果 File::open 成功,则 pattern 变量 file 中的文件句柄 变为可变变量中的值 username_file 并且函数继续。在 Err 情况下,我们不使用 panic!,而是使用返回 keyword 完全从函数中提前返回并传递 error 值 从 File::open,现在在模式变量 e 中,返回到调用代码作为此函数的 error 值。


因此,如果我们在 username_file 中有一个文件句柄,该函数就会在变量 username 中创建一个新的 String,并在 username_file 中对文件句柄调用 read_to_string 方法,以将文件内容读入 用户名read_to_string 方法还返回 Result,因为它可能会失败,即使 File::open 成功。因此,我们需要另一个匹配项来处理该 Result:如果 read_to_string 成功,则我们的函数成功,并且我们从现在位于 username 中的文件中返回 username 包裹在 Ok.如果read_to_string失败,我们将返回错误值,其方式与在处理 File::open 返回值的匹配项中返回错误值的方式相同。但是,我们不需要明确地说 return,因为这是函数中的最后一个表达式。


然后,调用此代码的代码将处理获取包含 username 的 Ok 值或包含 io::ErrorErr 值。由调用代码决定如何处理这些值。例如,如果调用代码获得 Err 值,它可以调用 panic!并导致程序崩溃,使用默认用户名,或从文件以外的其他位置查找用户名。我们没有足够的信息来了解调用代码实际尝试做什么,因此我们将所有成功或错误信息向上传播,以便它进行适当处理。


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


放在 Result 值后面的 被定义为与我们在示例 9-6 中为处理 Result 值而定义的 match 表达式的工作方式几乎相同。如果 Result 的值为 Ok,则 Ok 中的值将从此表达式返回,并且程序将继续。如果值是 Err,则 Err 将从整个函数返回,就像我们使用了 return 关键字一样,因此错误值会传播到调用代码。


示例 9-6 中的 match 表达式和 运算符的作用是不同的:调用 运算符的错误值会通过 from 函数,该函数在标准库的 From trait 中定义,用于将值从一种类型转换为另一种类型。当 运算符调用 from 函数时,收到的错误类型将转换为当前函数的返回类型中定义的错误类型。当函数返回一种错误类型来表示函数可能失败的所有方式时,这非常有用,即使部分可能由于许多不同的原因而失败。


例如,我们可以更改示例 9-7 中的 read_username_from_file 函数,以返回我们定义的名为 OurError 的自定义错误类型。如果我们还定义 impl From<io::Error> for OurError 构造一个 OurErrorio::Error 中调用,则 运算符在 read_username_from_file 将调用 FROM 并转换错误类型,而无需向函数添加更多代码。


在示例 9-7 的上下文中,File::open 调用末尾的 会将 Ok 中的值返回给变量 username_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:在 算子


我们已将 username 中新 String 的创建移至 函数;这部分没有改变。而不是创建变量 username_file,我们已经将对 read_to_string 的调用直接链接到 File::open(“hello.txt”)?.我们在 read_to_string调用,我们仍然返回一个包含 usernameOk 值 当 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 而不是打开然后读取文件


将文件读入 String 是一个相当常见的作,因此标准库提供了方便的 fs::read_to_string 函数,该函数打开文件,创建新的 String,读取文件的内容,将内容放入该 String 中,然后返回它。当然,使用 fs::read_to_string 没有给我们机会解释所有的错误处理,所以我们这样做了 长路先。


其中 可以使用 Operator


运算符 ? 只能在其返回类型与使用 的值 ? 兼容的函数中使用。这是因为 运算符被定义为从函数中提前返回一个值,方式与我们在示例 9-6 中定义的 match 表达式相同。在示例 9-6 中, match 使用的是 Result 值,并且早期返回分支返回了一个 Err(e) 值。函数的返回类型必须是 Result,以便与此返回兼容。


在示例 9-10 中,让我们看看如果我们在 main 函数中使用 运算符,并且返回类型与我们使用 的 ?on 的类型不兼容,我们会得到什么错误。


文件名: src/main.rs

use std::fs::File;

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


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


此代码将打开一个文件,这可能会失败。 运算符跟在 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 + 
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


这个错误指出我们只允许在返回 ResultOption 或其他实现 FromReresidual 的。


要修复此错误,您有两种选择。一种选择是更改函数的返回类型,使其与使用 运算符的值兼容,只要没有限制阻止这样做。另一种选择是使用匹配项Result<T, E> 方法之一来处理 Result<T, E> 以任何适当的方式。


错误消息还提到 也可以与 Option<T> 值一起使用。与使用 on Result 一样,您只能在返回 Option 的函数中使用 on 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()
}

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>,因为可能存在 字符,但也有可能没有。此代码采用 text string slice 参数并对其调用 lines 方法,该方法返回字符串中各行的迭代器。由于此函数要检查第一行,因此它在迭代器上调用 next 以从迭代器获取第一个值。如果 text 是空字符串,则对 next 的调用将返回 None,在这种情况下,我们使用 停止并从 None 返回 None last_char_of_first_line。如果 text 不是空字符串,则 next 将返回一个 Some 值,其中包含 text 中第一行的字符串切片。


这 ? 提取字符串 slice,我们可以在该字符串 slice 上调用 chars 来获取其字符的迭代器。我们对第一行中的最后一个字符感兴趣,因此我们调用 last 来返回迭代器中的最后一项。这是一个 Option,因为第一行可能是空字符串;例如,如果文本以空行开头,但在其他行上包含字符,如 “\nhi”。但是,如果第一行有最后一个字符,它将在 Some 变体中返回。中间的 运算符为我们提供了一种简洁的方式来表达这个逻辑,允许我们在一行中实现函数。如果我们不能在 Option 上使用 运算符,我们将不得不使用更多的方法调用或 match 表达式来实现这个逻辑。


请注意,您可以在返回 Result,您可以在返回 Option 的函数中对 Option 使用 运算符,但不能混合和匹配。运算符不会自动将 Result 转换为 Option,反之亦然;在这些情况下,您可以使用 Result 上的 OK 方法或 ok_or 上的 方法 选项显式执行转换。


到目前为止,我们使用的所有主要函数都返回 ()。main 函数很特殊,因为它是可执行程序的入口点和出口点,并且其返回类型可以是什么有限制,以便程序按预期运行。


幸运的是,main 还可以返回 Result<(),E>。示例 9-12 包含示例 9-10 中的代码,但我们已将 main 的返回类型更改为 结果<(),Box<dyn 错误>>,并在末尾添加了返回值 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 对象,我们将在 “使用允许不同 Types“部分。现在,您可以将 Box<dyn Error> 读作“任何类型的错误”。对 Result 使用 允许错误类型Box<dyn Error> 的主函数中的值,因为它允许提前返回任何 Err 值。即使此 main 函数的主体只会返回 std::io::Error 类型的错误,但通过指定 Box<dyn Error>,即使将更多返回其他错误的代码添加到 main 主体中,此签名也将继续正确。


main 函数返回 Result<(), E> 时,如果 main 函数返回 Ok(()),则可执行文件将以值 0 退出,如果 main 返回 Err 值。用 C 语言编写的可执行文件在退出时返回整数:成功退出的程序返回整数 0,出错的程序返回非 0 的整数。Rust 还从可执行文件中返回整数以与此约定兼容。


main 函数可以返回实现 std::p rocess::Termination trait,其中包含返回 ExitCode 的函数报告。有关为您自己的类型实现 Termination trait 的更多信息,请参阅标准库文档。


现在我们已经讨论了调用 panic!或返回 Result 的细节,让我们回到如何决定在哪些情况下使用哪个合适的主题。