参考:https://kaisery.github.io/trpl-zh-cn/ch09-00-error-handling.html
Rust 将错误组合成两个主要类别:可恢复错误recoverable )和 不可恢复错误unrecoverable )。可恢复错误通常代表向用户报告错误和重试操作是合理的情况,比如未找到文件。不可恢复错误通常是 bug 的同义词,比如尝试访问超过数组结尾的位置。
大部分语言并不区分这两类错误,并采用类似异常这样方式统一处理他们。Rust 并没有异常,但是,有可恢复错误 Result<T, E> ,和不可恢复(遇到错误时停止程序执行)错误 panic!

panic!:不可恢复错误

panic!宏:

  • 当执行这个宏时,程序会打印出一个错误信息,展开并清理栈数据,然后接着退出。出现这种情况的场景通常是检测到一些类型的 bug,而且程序员并不清楚该如何处理它。
  • 当出现 panic 时,程序默认会开始 展开unwinding ),这意味着 Rust 会回溯栈并清理它遇到的每一个函数的数据,不过这个回溯并清理的过程有很多工作。另一种选择是直接 终止abort ),这会不清理数据就退出程序。那么程序所使用的内存需要由操作系统来清理。如果你需要项目的最终二进制文件越小越好,panic 时通过在 Cargo.toml[profile] 部分增加 panic = 'abort',可以由展开切换为终止。例如,如果你想要在release模式中 panic 时直接终止:

    1. [profile.release]
    2. panic = 'abort'
  • panic 的报错信息会提示程序出错的位置:

    1. thread 'main' panicked at 'crash and burn', src/main.rs:2:5
    2. note: Run with `RUST_BACKTRACE=1` for a backtrace.
  • src/main.rs:2:52:5 指的就是 src/main.rs 文件 第 2 行 第 5 个字符的地方导致了 panic。
    panic! 可能会出现在我们的代码所调用的代码中。错误信息报告的文件名和行号可能指向别人代码中的 panic! 宏调用(即最原始出错的地方),而不是我们代码中最终导致 panic! 的那一行。

  • RUST_BACKTRACE 环境变量设置为任何不是 0 的值来获取完整 backtrace,此外还必须启用 debug 标识。当不使用 --release 参数运行 cargo build 或 cargo run 时 debug 标识会默认启用。

    Result:可恢复错误

    Result 是具有 Ok()Err() 两个成员的枚举体,成员类型是泛型参数的。因为 Result 有这些泛型类型参数,我们可以将 Result 类型和标准库中为其定义的函数用于很多不同的场景,这些情况中需要返回的成功值和失败值可能会各不相同。

    1. enum Result<T, E> {
    2. Ok(T),
    3. Err(E),
    4. }

    Option 枚举一样,Result 枚举和其成员也被导入到了 prelude 中,所以就不需要在 match 分支中的 OkErr 之前指定 Result::

    match 模式匹配 OkErr

    经典例子:打开文本文件

    1. use std::fs::File;
    2. fn main() {
    3. let f = File::open("hello.txt"); // 返回 Result 类型
    4. let f = match f {
    5. Ok(file) => file,
    6. Err(error) => {
    7. panic!("Problem opening the file: {:?}", error)
    8. },
    9. };
    10. }

    如果文件不存在,则得到 Err,并 panic ,打印如下输出:

    1. thread 'main' panicked at 'Problem opening the file: Error { repr: Os { code: 2, message: "No such file or directory" } }', src/main.rs:9:12

    通过查阅文档,可以发现File::open返回的Err成员中的值类型是enum: std::io::ErrorKind。据此我们可以区分更详细的错误来做不同的处理,比如“文件不存在”那么就创建它。在其他语言中也是类似的思路,而在 Rust 强大的类型下,“错误”就可以用枚举这种基本类型来描述,处理错误进而转换成匹配模式流程,这充分体现了抽象思考的魅力。以下是加入了用创建文件来处理文件不存在错误的代码:

    1. use std::fs::File;
    2. use std::io::ErrorKind;
    3. fn main() {
    4. let f = File::open("hello.txt");
    5. let f = match f {
    6. Ok(file) => file,
    7. Err(error) => match error.kind() {
    8. ErrorKind::NotFound => match File::create("hello.txt") {
    9. Ok(fc) => fc,
    10. Err(e) => panic!("Problem creating the file: {:?}", e),
    11. },
    12. other_error => panic!("Problem opening the file: {:?}", other_error),
    13. },
    14. };
    15. }

    .unwarp().except("...")

    就像处理 Option 类型那样, 有很多方式来消除处理 Result 时的多层 match 嵌套。比如遇到 Err 直接 panic:.unwrap().expect("desciption") 方法。
    如果 Result 值是成员 Okunwrap 会返回 Ok 中的值;如果 Result 是成员 Errunwrap 会为我们调用 panic!
    exceptunwrap 处理 Err 的基础上支持添加 panic 时打印的描述。即代替了这种形式的 match

    1. match expr {
    2. Ok(item) => item,
    3. Err(err) => panic!("description"),
    4. }

    因此 “上面打开文件的例子” 就可以用一行替代:let f = File::open("hello.txt").expect("Failed to open hello.txt");

    传播错误 和 ? 运算符

    当编写一个其实现会调用一些可能会失败的操作的函数时,除了在这个函数中处理错误外,还可以选择让调用者知道这个错误并决定该如何处理。后者做法被称为 传播propagating )错误,这样能更好的控制代码调用,因为调用者可能拥有更多信息或逻辑来决定应该如何处理错误。

    1. use std::io;
    2. use std::io::Read;
    3. use std::fs::File;
    4. fn read_username_from_file() -> Result<String, io::Error> {
    5. let f = File::open("hello.txt");
    6. // 可能遇到的错误 1:打开文件时出错
    7. let mut f = match f {
    8. Ok(file) => file,
    9. Err(e) => return Err(e),
    10. };
    11. let mut s = String::new();
    12. // 可能遇到的错误 2:读取时出错
    13. match f.read_to_string(&mut s) {
    14. Ok(_) => Ok(s),
    15. Err(e) => Err(e),
    16. }
    17. }

    调用这个函数的代码最终会得到一个包含用户名的 Ok 值,或者一个包含 io::ErrorErr 值。我们无从得知调用者会如何处理这些值。例如,如果他们得到了一个 Err 值,他们可能会选择 panic! 并使程序崩溃、使用一个默认的用户名或者从文件之外的地方寻找用户名。我们没有足够的信息知晓调用者具体会如何尝试,所以将所有的成功或失败信息向上传播(向外告知),让他们选择合适的处理方法。
    Result 值之后使用 ? 表示:

  • 如果 Result 的值是 Ok,这个表达式将会返回 Ok 中的值而程序将继续执行;

  • 如果值是 ErrErr 中的值将作为整个函数的返回值,就好像使用了 return 关键字一样,这样错误值就被传播给了调用者。

“上述代码” 的转换成 ? 的写法:

  1. use std::io;
  2. use std::io::Read;
  3. use std::fs::File;
  4. fn read_username_from_file() -> Result<String, io::Error> {
  5. let mut f = File::open("hello.txt")?;
  6. let mut s = String::new();
  7. f.read_to_string(&mut s)?;
  8. Ok(s)
  9. }

很明显,? 运算符消除了大量样板代码并使得函数的实现更简单。我们甚至可以在 ? 之后直接使用链式方法调用来进一步缩短代码:

  1. use std::io;
  2. use std::io::Read;
  3. use std::fs::File;
  4. fn read_username_from_file() -> Result<String, io::Error> {
  5. let mut s = String::new();
  6. File::open("hello.txt")?.read_to_string(&mut s)?;
  7. Ok(s)
  8. }

将文件读取到一个字符串是相当常见的操作,所以 Rust 提供了名为 fs::read_to_string 的函数,它会打开文件、新建一个 String、读取文件的内容,并将内容放入 String,接着返回它。所以这个例子最终可以用一行语句达到相同的效果:

  1. use std::io;
  2. use std::fs;
  3. fn read_username_from_file() -> Result<String, io::Error> {
  4. fs::read_to_string("hello.txt")
  5. }

?match 表达式不同的地方:

  1. 只能在返回 Result 或者其它实现了 std::ops::Try 的类型的函数中使用 ? 运算符。main 函数是特殊的,其必须返回什么类型是有限制的。main 函数的一个有效的返回值是 (),另一个有效的返回值是 Result<T, E>

    1. use std::error::Error;
    2. use std::fs::File;
    3. fn main() -> Result<(), Box<dyn Error>> {
    4. let f = File::open("hello.txt")?;
    5. Ok(())
    6. }
  2. Box<dyn Error> 被称为 “trait 对象”(“trait object”)。#todo: trait# 目前可以理解 Box<dyn Error> 为使用 ?main 允许返回的 “任何类型的错误”。

  3. ? 运算符所使用的错误值被传递给了 from 函数,它定义于标准库的 From trait 中,其用来将错误从一种类型转换为另一种类型。当 ? 运算符调用 from 函数时,收到的错误类型被转换为由当前函数返回类型所指定的错误类型。这在当函数返回单个错误类型来代表所有可能失败的方式时很有用,即使其可能会因很多种原因失败。只要每一个错误类型都实现了 from 函数来定义如何将自身转换为返回的错误类型,? 运算符会自动处理这些转换。

    是否应该 panic!

    panic! 宏代表一个程序无法处理的状态,并停止执行而不是使用无效或不正确的值继续处理。Rust 类型系统的 Result 枚举代表操作可能会在一种可以恢复的情况下失败。可以使用 Result 来告诉代码调用者他需要处理潜在的成功或失败。
    返回 Result 是定义可能会失败的函数的一个好的默认选择。你可以选择对任何错误场景都调用 panic!,不管是否有可能恢复,不过这样就是你代替调用者决定了这是不可恢复的。选择返回 Result 值的话,就将选择权交给了调用者,而不是代替他们做出决定。调用者可能会选择以符合他们场景的方式尝试恢复,或者也可能干脆就认为 Err 是不可恢复的,所以他们也可能会调用 panic! 并将可恢复的错误变成了不可恢复的错误。
    示例、代码原型和测试都非常适合 panic ,当你编写一个示例来展示一些概念时,在拥有健壮的错误处理代码的同时也会使得例子不那么明确。例如,调用一个类似 unwrap 这样可能 panic! 的方法可以被理解为一个你实际希望程序处理错误方式的占位符,它根据其余代码运行方式可能会各不相同。类似地,在我们准备好决定如何处理错误之前,unwrap和expect方法在原型设计时非常方便。当我们准备好让程序更加健壮时,它们会在代码中留下清晰的标记。如果方法调用在测试中失败了,我们希望这个测试都失败,即便这个方法并不是需要测试的功能。因为 panic! 会将测试标记为失败,此时调用 unwrap 或 expect 是恰当的。
    当我们比编译器知道更多的情况:当你有一些其他的逻辑来确保 Result 会是 Ok 值时,调用 unwrap 也是合适的,虽然编译器无法理解这种逻辑。你仍然需要处理一个 Result 值:即使在你的特定情况下逻辑上是不可能的,你所调用的任何操作仍然有可能失败。如果通过人工检查代码来确保永远也不会出现 Err 值,那么调用 unwrap 也是完全可以接受的
    参考:https://kaisery.github.io/trpl-zh-cn/ch09-03-to-panic-or-not-to-panic.html