Rust 语言从入门到实战 唐刚 学习笔记18

进阶篇 (7讲)

18|错误处理系统:错误的构建、传递和处理

学习 Rust 语言中的错误表示、传递及处理。

Rust 的错误处理很重要,也很系统。

错误的分类

错误要分类,不同类型的错误有不同的处理策略。

分 可恢复型错误 和 不可恢复型错误 这两种。

不可恢复型错误

碰到了,要及早退出程序,或直接不启动程序,而是打出错误信息。

支持设施有 4 个:

  • panic!:让程序直接崩掉,退出程序,可选是否打印出 栈回溯信息
  • todo!:功能还未实现,执行到这里直接退出程序,提示:本功能待完善。
  • unimplemented!:功能还未实现,执行到这里直接退出程序,提示:本功能未实现。
  • unreachable!:原则上不可能执行到的语句位置,如 loop {} 死循环的后面,若执行到,肯定出错了,直接退出程序

可恢复型错误

遇到后,不该让程序直接停止运行,要在代码逻辑中分析可能的错误原因,要么尝试恢复性处理,要么返回自定义的错误信息(让用户明白任务未达到预期的原因)。

Rust 中,用 Result 来承载可能会出错的函数返回值。

Result 中: T 正常情况下的返回类型,E 出错情况下的返回类型。如何方便地构造 E 的实例,是一个重要的课题。

不可恢复型错误和可恢复型错误在某些时候界限不是那么清晰,在同一个地方,你既可以把它设计成可恢复型错误,也可以把它处理成不可恢复型错误。因此从本质上来说,是由业务逻辑来确定你要把它当成哪一类错误来处理。

错误相关的基础设施

Result

Rust 用 Result 类型(实例)来包裹错误类型(实例)。

Result 通过函数返回值返回到上层调用者函数,由上层函数来决定是在本层处理错误,还是继续把错误抛到更上一层函数。可把包裹错误的 Result 转换成 panic!() 或在 match 的 Err 分支调用 panic!() 来中止程序运行。

Result 解包的两个函数:unwrap() 和 expect(),作用是把错误 Result 值转换成 panic!()

两个方法的区别仅在于 expect() 可提供更多的信息(提供这个 panic 的精确位置),前提是这个提示信息要独一无二。在调用栈层次非常深的情况下,提供准确的 panic 位置信息非常重要

错误处理本身是一项系统性的工作:

  1. 错误的表示和构造;
  2. 错误的传递;
  3. 错误的处理。

软件项目到成熟阶段,用于错误处理的代码会占总代码的三分之一以上。

Rust 从语法和标准库层面,对错误处理做了根本上的原生支持,必须对程序的健壮性负责,至少也要留下明显足迹

演示Rustc 如何提醒我们。

// 
fn foo() -> Result {
    Ok("abc".to_string())
}
fn main() {
    foo();  // 这里Rustc小助手会警告你
}

其他语言中,若像示例那样,调用 foo() 函数、忽略了它的返回值,一般没问题。

Rust 会提醒,有一个 Result 没被使用,必须要处理,并给出了处理建议。

// 
warning: unused `Result` that must be used
 --> src/main.rs:5:5
  |
5 |     foo();  // 这里Rustc小助手会警告你
  |     ^^^^^
  |
  = note: this `Result` may be an `Err` variant, which should be handled
  = note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
  |
5 |     let _ = foo();  // 这里Rustc小助手会警告你
  |     +++++++

小助手不会放过任何一个可能的错误

Error trait

Rust 中定义了一个抽象的 Error trait,任何实现了这个 trait 的类型都被 Rust 生态认为是一个错误类型。

// 
pub trait Error: Debug + Display {}

一个类型要实现 Error,必须同时为这个类型实现 Debug 和 Display trait(Debug trait 可用 derive 宏自动实现,而 Display trait 要手动 impl 实现)。

实现 Error trait 是 Rust 生态中的一种规范。若在本层 module 里定义错误类型,为它实现了 Debug、Display、Error ,生态就认成是一种错误类型。--> trait 作为社区协议的作用。

实现了 Error trait 的类型,可被代入到 dyn Error 这个 trait object 里使用。生态中很多库支持接受 &dyn Error 或 Box 类型,代码就能和那些库无缝集成了。

如,让自定义的错误类型从下面的函数中返回。

// 
fn foo() -> Result> {
  // 可返回一个由Box包起来的自定义错误类型。
}

这并不是说, std::result::Result 类型里的 E 就一定要实现 Error trait。Result 中的 E 可以是任意类型

标准库里 Result 的定义:

// 
pub enum Result {
    Ok(T),
    Err(E),
}

从这个 enum 可以看到,对 E 没有任何约束。即使你的类型不实现 Error trait,它还是能被代入 Result 中作为错误类型而使用。请注意这点区别。

Result 的变体 Ok 和 Err 已经被加入到 pub std::prelude 里,代码中可直接使用,不需加 Result 前缀。

std 中的错误类型

Rust 里定义的错误类型,常用的有:

std::io:Error

io Error,一个结构体。标准库里 I/O 相关场景的错误类型。

// 
pub struct Error { /* private fields */ }

常见的 I/O 操作:标准输入输出、网络读写、文件读写等。

std::io 模块对这种 I/O 读写做了统一抽象,而类型 io::Error 也是这个抽象里的一部分。

可用 new() 函数创建一个 io::Error 实例,第一个参数是 ErrorKind 枚举,第二个参数是具体的错误内容。

// 
use std::io::{Error, ErrorKind};

// 错误可以从字符串中构造
let custom_error = Error::new(ErrorKind::Other, "oh no!");

其中 ErrorKind 是一个枚举(目前包含 40 种变体),标准库中能遇到的 I/O 错误都定义了。

// 
pub enum ErrorKind {
    NotFound,          // 未找到
    PermissionDenied,  // 权限被拒绝;拒绝访问
    ConnectionRefused,  // 连接被拒绝
    ConnectionReset,    // 连接被重置
    HostUnreachable,    // 主机无法访问
    NetworkUnreachable, // 网络无法访问
    ConnectionAborted,  // 连接中止
    NotConnected,       // 未连接
    AddrInUse,          // Addr在使用中
    AddrNotAvailable,   // Addr不可用
    NetworkDown,     // 网络故障
    BrokenPipe,      // 管道破裂
    AlreadyExists,   // 已经存在
    WouldBlock,      // 将阻止
    NotADirectory,   // 不是个目录
    IsADirectory,    // 是个目录
    DirectoryNotEmpty,   // 目录不为空
    ReadOnlyFilesystem,  // 只读文件系统
    FilesystemLoop,      // 文件系统循环
    StaleNetworkFileHandle, // 过时的网络文件句柄
    InvalidInput,    // 无效输入
    InvalidData,     // 无效数据
    TimedOut,        // 超时
    WriteZero,       // 写入零
    StorageFull,     // 存储已满
    NotSeekable,     // 不可搜索
    FilesystemQuotaExceeded,  // 超过文件系统配额
    FileTooLarge,     // 文件过大
    ResourceBusy,     // 资源繁忙
    ExecutableFileBusy,  // 可执行文件忙
    Deadlock,          // 僵局,死锁
    CrossesDevices,    // 跨设备
    TooManyLinks,      // 链接太多
    InvalidFilename,   // 无效文件名
    ArgumentListTooLong,   // 参数列表太长
    Interrupted,       // 被中断
    Unsupported,       // 不被支持
    UnexpectedEof,     // 出乎意料的Eof
    OutOfMemory,       // 内存不足
    Other,             // 其他
}

远远不能覆盖全部(ErrorKind 仅用于标准库 I/O 模块中可能出现的的错误类型)

parseError

标准库中定义了一组 parse 相关的错误类型。

  • std::num::ParseIntError
  • std::num::ParseFloatError
  • std::char::ParseCharError
  • std::str::ParseBoolError
  • std::net::AddrParseError

这些错误类型与 FromStr trait 相关,即:把字符串解析到其他类型时可能会出现的错误类型:

// 
use std::net::IpAddr;
fn main() {
    let s = "100eee";
    if let Err(e) = s.parse::() {
        // e 这里是 ParseIntError
        println!("Failed conversion to i32: {e}");
    }

    let addr = "127.0.0.1:8080".parse::();
    if let Err(e) = addr {
        // e 这里是 AddrParseError
        println!("Failed conversion to IpAddr: {e}");
    }
}
// 输出
Failed conversion to i32: invalid digit found in string
Failed conversion to IpAddr: invalid IP address syntax

用枚举定义错误

在 Rust 中用 Result 作为函数返回值,在上层中处理的典型方式。

// 
// 定义自己的错误类型,一般是一个枚举,因为可能有多种错误
enum HereError {
    Error1,
    Error2,
    Error3,
}
// 一个函数返回Err
fn bar() -> Result {
    Err(HereError::Error3)
}

fn foo() {
    match bar() {
        Ok(_) => {}
        Err(err) => match err {  // 在上层中通过match进行处理
            HereError::Error1 => {}
            HereError::Error2 => {}
            HereError::Error3 => {}
        },
    }
}

通常在当前模块中定义错误类型(枚举类型,错误种类往往不止一个)。若接口返回了这个错误类型,上层就要 match 这个枚举类型进行错误处理。

到目前为止,并没给自定义的错误类型 HereError 实现 Debug、Display 和 Error trait,所以我们的错误类型还仅限于自己玩。

为了把它纳入 Rust 生态体系,需要给它实现这 3 个 trait。但没必要自己手动去实现,社区的工具 crate: thiserror 可实现这个目的。

错误的传递

前面介绍了错误类型的定义和处理的基本方式,系统性地介绍错误的传递。

函数返回 Result

若函数中有可能会出错,那它的返回值就默认约定为 Result。

先对比其他语言是怎么处理的。

  • C 用同一种类型的特殊值表示异常。如函数返回一个有符号整数,用 0 表示正常返回,用 -1 或其他负数值表示异步返回。但这个约定并不是普遍共识,大部分函数返回 0 表示正常,但特定情况下,返回 0 又表示不正确。缺乏强制约束给整个生态带来了混乱
  • Java 提供强大的 try-catch-throw,在语言层面捕获异常。方便,但给语言 Runtime 带来负担(语言的 Runtime 负责捕获代码中的异常,有额外的性能损失)。由于 try-catch-throw 很方便,为了偷懒,将一大段代码全部包在 try-catch-throw 中,会大大降低代码的质量,程序没办法对错误情况做精细地处理

Rust :把异常情况独立成一个维度,放在 Result 的 Err 变体中。错误在类型上就以独立的维度存在。

// 
fn foo(num: u32) -> Result {
    if num == 10 {
        Ok("Hello world!".to_string())
    } else {
        Err("I'm wrong!".to_string())
    }
}

代码中的错误类型部分被定义为 String 类型(实际可定义成任意类型)。

把错误定义成 u32 类型。

// 
fn foo(num: u32) -> Result {
    if num == 10 {
        Ok("Hello world!".to_string())
    } else {
        Err(100)
    }
}

有时函数中的错误情况可能不止一种,返回类型惯用是 enum:

// 
enum MyError {
    Error1,
    Error2,
    Error3,
}

fn foo(num: u32) -> Result {
    match num {
        10 => Ok("Hello world!".to_string()),
        20 => Err(MyError::Error1),
        30 => Err(MyError::Error2),
        _ => Err(MyError::Error3),
    }
}

这里 Result 的 E 部分,类型就是自定义的 MyError

另一种常用办法,让函数返回 Result<_, Box>:

// 
use std::error::Error;
use std::fmt;

#[derive(Debug)]
struct MyError;

impl fmt::Display for MyError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self)
    }
}

impl Error for MyError {}

fn foo(num: u32) -> Result> {
    match num {
        10 => Ok("Hello world!".to_string()),
        _ => {
            let my_error = MyError;
            Err(Box::new(my_error))
        }
    }
}

把错误独立到另一个维度来处理,得到了相当大的灵活性和安全性:可借助类型系统来检查正常情况与异常情况的不同返回,大大减少了编码出错的机率。

有了这套优秀的错误处理底层设施后,整个 Rust 生态上层建筑逐渐结构性地构建起来了,大家都遵从这个约定,用同样的方式来传递和处理错误,形成了一个健康良好的生态。

map_err 转换错误类型

常用 Result 上的 map_err 方法手动转换错误类型

// 
use std::fs::File;
use std::io::Read;

fn read_file() -> Result {
    match File::open("example.txt").map_err(|err| format!("Error opening file: {}", err)) {
        Ok(mut file) => {
            let mut contents = String::new();
            match file
                .read_to_string(&mut contents)
                .map_err(|err| format!("Error reading file: {}", err))
            {
                Ok(_) => Ok(contents),
                Err(e) => {
                    return Err(e);
                }
            }
        }
        Err(e) => {
            return Err(e);
        }
    }
}

要在 read_file() 中打开一个文件,并读取文件全部内容到字符串中。

过程中,可能出现两个 I/O 错误:打开文件错误、读取文件错误。

用 map_err 将这两个 I/O 错误的类型都转换成了 String 类型,来和函数返回类型签名相匹配。然后,对两个操作的 Result 进行了 match 匹配。这里的两个文件操作可能的错误都是 std::io::Error 类型的。

一个函数中会产生不同的错误类型,用 map_err 显式地不同的错误类型转换成同一种错误类型

Result 链式处理

除了每次对 Result 进行 match 处理外,Rust 还流行一种方式:对 Result 进行链式处理

将打开文件并读取内容,改写成链式调用的风格:

// 
use std::fs::File;
use std::io::Read;

fn read_file() -> Result {
    File::open("example.txt")
        .map_err(|err| format!("Error opening file: {}", err)) 
        .and_then(|mut file| {
            let mut contents = String::new();
            file.read_to_string(&mut contents)
                .map_err(|err| format!("Error reading file: {}", err))
                .map(|_| contents)
        })
}

fn main() {
    match read_file() {
        Ok(n) => println!("{}", n),
        Err(err) => println!("Error: {}", err),
    }
}

链式风格比用 match 处理的简洁很多。

这里用到 map_err、and_then、map 三种链式操作,可在不解开 Result 包的情况下直接对里面的内容进行处理。关于这几个方法的详细内容,参考第 8 讲。

第 5 行 File::open() 执行完,若 Result 是 Err,那在第 6 行 map_err() 后,不会再走 and_then() 操作,而是直接从 read_file() 函数中返回这个 Result 了。若第 5 行的产生的 Result 是 Ok,就会跳过第 6 行,进入第 7 行执行。

进入第 7 行后,会消解前面产生的 Result,把 file 对象传进来使用。

再看第 9 行产生的 Result,若这个 Result 实例是 Err,那执行完第 10 行后,就直接从闭包返回了,返回的是 Err 值,这个值会进一步作为 read_file() 函数的返回值返回。若 Result 实例是 Ok,就会跳过第 10 行,执行第 11 行,第 11 行将 contents 字符串 move 进来作为内层闭包的返回值,并进一步以 Ok(contents) 的形式作为 read_file() 函数的返回值返回。

链式处理比 match 操作优美太多,但理解起来也困难太多。慢慢理解,熟能生巧。

? 问号操作符

match 写法,怎么简化?

// 
use std::fs::File;
use std::io::Read;

fn read_file() -> Result {
    match File::open("example.txt").map_err(|err| format!("Error opening file: {}", err)) {
        Ok(mut file) => {
            let mut contents = String::new();
            match file
                .read_to_string(&mut contents)
                .map_err(|err| format!("Error reading file: {}", err))
            {
                Ok(_) => Ok(contents),
                Err(e) => {
                    return Err(e);
                }
            }
        }
        Err(e) => {
            return Err(e);
        }
    }
}

上面的第 13~15 行和第 18~20 行,都是把错误 Err(e) 返回到上一层

// 
  Err(e) => {
      return Err(e);
  }

Rust 用 ?操作符来简化这种场景。

// 
use std::fs::File;
use std::io::Read;

fn read_file() -> Result {
    let mut file =
        File::open("example.txt").map_err(|err| format!("Error opening file: {}", err))?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)
        .map_err(|err| format!("Error reading file: {}", err))?;

    Ok(contents)
}

神奇!行数缩短了这么多。

? 操作符大体上等价于一个 match 语句

// 
let ret = a_result?;
等价于
let ret = match a_result {
    Ok(ret) => ret,                
    Err(e) => return Err(e),   // 注意这里有一个return语句。
};

若 result 的值是 Ok,就解包;若是 Err,就提前返回 Err。

--> 一种防御式编程,遇到了错误就提前返回。让代码大大简化,少很多层括号。

这里的 e 是这个 a_result 里 Err(e) 中的 e。这个实例的类型是什么呢?

用 return 返回它,它是不是和函数中定义的返回类型中的错误类型一致呢?这个问题其实很重要。上例,明确地用 map_err 把 io::Error 转换成了 String 类型,所以没问题。

把 map_err 去掉试试。

// 
use std::fs::File;
use std::io::Read;

fn read_file() -> Result {
    let mut file =
        File::open("example.txt")?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;

    Ok(contents)
}

// 编译器报错了:

error[E0277]: `?` couldn't convert the error to `String`
 --> src/lib.rs:6:34
  |
4 | fn read_file() -> Result {
  |                   ---------------------- expected `String` because of this
5 |     let mut file =
6 |         File::open("example.txt")?;
  |                                  ^ the trait `From` is not implemented for `String`
  |
  = note: the question mark operation (`?`) implicitly performs a conversion on the error value using the `From` trait
  = help: the following other types implement trait `From`:
            >
            >>
            >>
            >
            >
            >
  = note: required for `Result` to implement `FromResidual>`

error[E0277]: `?` couldn't convert the error to `String`
 --> src/lib.rs:8:39
  |
4 | fn read_file() -> Result {
  |                   ---------------------- expected `String` because of this
...
8 |     file.read_to_string(&mut contents)?;
  |                                       ^ the trait `From` is not implemented for `String`
  |
  = note: the question mark operation (`?`) implicitly performs a conversion on the error value using the `From` trait
  = help: the following other types implement trait `From`:
            >
            >>
            >>
            >
            >
            >
  = note: required for `Result` to implement `FromResidual>`

提示:? 操作符不能把错误类型转换成 String 类型。

--> 用 ?的常见错误,错误类型不一致。

错误提示:? 操作符用 From trait 尝试对错误类型做隐式转换,列出了几种已经实现了的可以转换到 String 的错误类型。Rust 处理 ? 操作符时,会尝试对错误类型进行转换,看能不能自动把错误类型转换到函数返回类型中的那个错误类型。若不行,就报错。(第 11 讲,如何用 From trait。)

按要求对 std::io::Error 实现一下这个转换:

// 
use std::fs::File;
use std::io::Read;

impl From for String {
    fn from(err: std::io::Error) -> Self {
        format!("{}", err)
    }
}

fn read_file() -> Result {
    let mut file =
        File::open("example.txt")?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;

    Ok(contents)
}

// 咦,不通过,提示:

error[E0117]: only traits defined in the current crate can be implemented for types defined outside of the crate
 --> src/lib.rs:4:1
  |
4 | impl From for String {
  | ^^^^^--------------------^^^^^------
  | |    |                        |
  | |    |                        `String` is not defined in the current crate
  | |    `std::io::Error` is not defined in the current crate
  | impl doesn't use only types from inside the current crate
  |
  = note: define and implement a trait or new type instead

违反第 9 讲说过的 trait 孤儿规则。

怎么解决呢?重新定义一个自己的类型:

// 
use std::fs::File;
use std::io::Read;

struct MyError(String);  // 用newtype方法定义了一个新的错误类型

impl From for MyError {
    fn from(err: std::io::Error) -> Self {
        MyError(format!("{}", err))
    }
}

fn read_file() -> Result {
    let mut file =
        File::open("example.txt")?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;

    Ok(contents)
}

行了。

在第 4 行用 newtype 模式定义了一个自定义错误类型,里面包了 String 类型,在第 6 行对它实现 From,在第 8 行产生了错误类型实例。在第 12 行,把 read_file() 的返回类型改成了 Result

若出现打开文件错误或读取文件错误,? 操作符会自动把 std::io::Error 类型转换到 MyError 类型上去,从 read_file() 函数里返回。不需每次手动写 map_err 转换错误类型了。--> 不错的解决方案。

用 ? 操作符,可在函数的嵌套调用中实现冒泡式的错误向上传递的效果。

错误处理系统最佳实践

错误的冒泡

通常我们编写的软件有很多依赖,在每个依赖甚至每个模块中,可能都有对应的错误子系统设计。一般会以一个 crate 为边界暴露出对象的错误类型及可能相关的处理接口。因此,如果我们从依赖树的角度来看,你编写的软件的错误系统也是以树的形式组织起来的,是一个层级系统。

层级错误系统中,某一层出现的错误,有的在本层处理,有的用冒泡的方式传递到更上层来处理。 ?操作符是编写冒泡错误处理范式的便捷设施。

从下层传上来的错误,该在哪个层次进行处理?由具体的软件架构设计决定,具体问题具体分析。

完整的错误系统包括:错误的构造和表示、错误的传递、错误的处理。

错误的构造和表示,很棒的库:thiserror。(thiserror - Rust)

错误的表示最佳实践

自定义的错误类型得实现 std::error::Error 这个 trait,才是在生态意义上讲合格的错误类型

        完整的软件,有非常多的错误类型,自己实现太繁琐。

库 thiserror,可以一体化标注式地生成那些样板代码。

用 thiserror 的方式:

// 
use thiserror::Error;    // 引入宏

#[derive(Error, Debug)]  // 这里derive Error宏
pub enum DataStoreError {
    #[error("data store disconnected")]  // 属性标注
    Disconnect(#[from] io::Error),       // 属性标注

    #[error("the data for key `{0}` is not available")]
    Redaction(String),

    #[error("invalid header (expected {expected:?}, found {found:?})")]
    InvalidHeader {
        expected: String,
        found: String,
    },

    #[error("unknown data store error")]
    Unknown,
}

用 thiserror,可直接在枚举上 drive Error 宏。大宏下面,还可用 #[error("")]、#[from] 等属性宏对枚举的变体做更多的配置。

通过这样的标注,把目标类型转换成了一个合格的被 Rust 生态认识的错误类型。

错误的传递最佳实践

Rust 中用 ? 操作符进行错误的冒泡传递。

? 返回的错误类型可能与函数返回值定义的错误类型不一样,这时,要手动做 map_err,手动实现 From trait,或用 thiserror 里提供的 #[from] 属性宏标注。

错误处理最佳实践

错误处理:对传过来的错误进行处理。

anyhow crate,快速(无脑)地接收和处理错误。统一用 Result 或等价的 anyhow::Result 作为一个函数的返回类型,担当错误的接收者。

以前要自己定义模块级的 Result,才能简写 std::result::Result。模块层级多了后,光维护这些 Result 类型,都是一件头痛的事情。

// 
struct MyError;
type Result = std::result::Result;

现在不需要自定义 Result type 。直接用 anyhow::Result 就可以。

// 
fn foo() -> anyhow::Result {}

实际是在更高的层次上定义了一种错误接收协议——任何模块,大家用 anyhow::Result 就行了,更方便。

用 anyhow::Result 作函数返回值,函数中可用 ?操作符把错误向上传递,只要这个错误类型实现了 std::error::Error 这个 trait 就行了。而我们前面讲过,这个 trait 是 std 标准库中的一个标准类型,如果你想让自己的错误类型融入社区,都应该实现这个 trait。而前面的 thiserror 也是方便实现这个 trait 的一个工具库。这样是不是一下子就串起来了。

std、anyhow 和 thiseror 可以无缝配合起来使用。

不再为错误类型的不一致,即向上传递的错误类型与函数返回值的错误类型不一致,而头痛了。

// 
use anyhow::Result;

fn get_cluster_info() -> Result {
    let config = std::fs::read_to_string("cluster.json")?;
    let map: ClusterMap = serde_json::from_str(&config)?;
    Ok(map)
}

注意,第 4 行返回的错误类型和第 5 行返回的错误类型是不同的,但都能无脑地扔给 anyhow::Result,因为它们都实现了 std::error::Error trait。

用 anyhow::Result 接收到错误实例后,用 match 结合 downcast 系列函数进行错误处理。

// 
match root_cause.downcast_ref::() {
    Some(DataStoreError::Censored(_)) => Ok(..),
    None => Err(error),
}

anyhow::Result 定义的是统一的错误类型接收载体,实际处理时,要把错误还原成原来的类型,分别进行处理(需要 downcast 的原因),语法就和上面的示例差不多。

隐含的知识点,anyhow::Error 保留了错误实例的原始类型信息,能做正确的错误处理分派。

anyhow 提供了辅助宏,简化错误实例的生成。

anyhow! 宏,快速构造出一次性的能被 anyhow::Result 接收的错误。

// 
return Err(anyhow!("Missing attribute: {}", missing));

anyhow 这个 crate 直接把 Rust 的错误处理的体验提升了一个档次,以统一的形式设计项目的错误处理系统。std、anyhow 和 thiserror 一起构成了 Rust 语言错误处理的最佳实践。

注:更多 anyhow 的资料,请查阅链接。(anyhow - Rust)

小结

系统性地介绍了 Rust 语言的错误处理相关的知识,介绍了 错误处理的最佳实践。

Rust 本身只提供了一些基础设施,Result、std::error::Error trait、map_err、? 表达式、From trait、panic! 系列宏等等。比较繁琐,标准也没办法统一。

社区 thiserror、anyhow 等优秀的 crate,使 Rust 语言错误处理的工程体验提升了一个层次,既好用又优美。这两个库充分展现了 Rust 语言强大的表达能力,anyhow 主要通过 Rust 强大的类型系统实现,thiserror 主要通过 Rust 强大的宏能力实现,而宏正是我们下节课要讲到的内容。

错误处理是软件中非常重要的组成部分,是软件稳定性和安全性的根源所在。Rust 语言强迫我们必须做完善的错误处理,这也是 Rust 语言与众不同的地方。

Rust 语言从入门到实战 唐刚 学习笔记18_第1张图片

// 

思考题

  1. 查阅 Rust std 资料,说说对 std::error::Error 的理解。
  2. 说明 anyhow::Error 与自定义枚举类型用作错误的接收时的区别。

你可能感兴趣的:(Rust,语言从入门到实战,唐刚,学习笔记,rust,学习,笔记)