因为不会处理Rust的错误类型,我差点被裁员了

序言

作为一名刚入门 Rust 的开发者,在构建库时,我原本对于 Rust 中错误处理的最佳实践并不了解。在开发 svgr-rs 这个库的过程中,我一开始只是简单地返回 String 来描述错误。然而,一位社区成员在 issue 中提出了建议,应当使用标准的 Error 类型来表示错误。我对此感到困惑,不太明白为何 Error 类型比 String 更恰当。

最近我深入研究了 Rust 官方提供的错误类型处理指南,并对如何在 Rust 中高效且优雅地处理错误有了深入理解。在这篇文章中,我不仅会分享这些指南的要点,还会讲解错误类型的优雅处理方式,并介绍 anyhowthiserror 这两个旨在简化错误处理流程的库。

Rust 错误类型处理指南

在 Rust 库中,当公共函数返回 Result<T, E> 类型时,类型 E 通常被用作错误类型。为了遵循 Rust 的错误处理惯例,错误类型应实现 std::error::Error trait。这是错误处理库对不同错误类型进行抽象的机制,并且它允许将一个错误用作另一个错误的源( source())。

另外,错误类型应实现 SendSync 特征。一个不是 Send 的错误不能被一个使用 thread::spawn 运行的线程返回。一个不是 Sync 的错误不能使用 Arc 在线程间传递。这些是多线程应用中基本错误处理的常见要求。

SendSync 对于能够使用 std::io::Error::new 将自定义错误封装进一个 IO 错误也很重要,这需要一个 Error + Send + Sync 的特征约束。

对于这项原则,一个需要保持警惕的地方是在返回错误的函数中,例如 reqwest::Error::get_ref。通常 Error + Send + Sync + 'static 对于调用者最为有用。添加 'static 生命周期使得特征对象可以和 Error::downcast_ref 一起使用。

永远不要使用 () 作为错误类型,即使错误没有有用的附加信息可以携带。

  • () 不实现 Error 特征,因此它不能和像 error-chain 这样的错误处理库一起使用。
  • () 不实现 Display 特征,因此如果用户想要因为错误而失败(fail),他们需要自己编写错误消息。
  • () 对于决定 unwrap() 错误的用户来说有一个无用的 Debug 表现形式。
  • 对于下游库来说,实现 From<()> 至它们错误类型并没有语义意义,所以 () 作为错误类型不能和 ? 操作符一起使用。

相反,为你的库或单个函数定义一个有意义的错误类型。提供适当的 ErrorDisplay 实现。如果错误没有有用的信息可以携带,它可以作为一个单元结构体来实现。

rust 复制代码
#![allow(unused)]
fn main() {
use std::error::Error;
use std::fmt::Display;

// 不要这样做...
fn do_the_thing() -> Result<Wow, ()>

// 要这样做...
fn do_the_thing() -> Result<Wow, DoError>

#[derive(Debug)]
struct DoError;

impl Display for DoError { /* ... */ }
impl Error for DoError { /* ... */ }
}

不应该实现 Error::description()。它已被弃用,用户应始终使用 Display 而不是 description() 来打印错误。

错误信息示例

显示错误类型的错误消息应该是小写的,不带尾标点,并且通常简洁。

  • "unexpected end of file"
  • "provided string was not true or false"
  • "invalid IP address syntax"
  • "second time provided was later than self"
  • "invalid UTF-8 sequence of {} bytes from index {}"
  • "environment variable was not valid unicode: {:?}"

标准库中的错误类型示例

在 Rust 的标准库中,字符串类型 str 提供了一个 parse 方法,它用于转换字符串到其他类型。例如,当你试图把一个字符串转换成 bool 类型,你需要调用这个 parse 方法,并指定你期望的结果类型为 bool。如果该字符串不是 "true""false"parse 方法将无法解析它为 bool 类型,并会返回一个错误 std::str::ParseBoolError

rust 复制代码
fn main() {
    match "true".parse::<bool>() {
        Ok(result) => println!("parse result: {:?}", result),
        // 这里的 err 类型为 std::str::ParseBoolError
        Err(err) => println!("parse error: {:?}", err)
    }
}

std::str::ParseBoolError 代表了一个特定的错误情况:提供的字符串既不等于 "true" 也不等于 "false"。它是一个没有任何字段的结构体,仅用来表示一个错误的状态。这个结构体实现了基础的 trait DebugClonePartialEqEq。它还实现了 fmt::Display trait 的 fmt 方法,当需要时可以打印出友好的错误提示信息。

rust 复制代码
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseBoolError;

impl fmt::Display for ParseBoolError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        "provided string was not `true` or `false`".fmt(f)
    }
}

跟踪和报告错误链

在 Rust 中,std::error::Error trait 的 source 方法提供了一种途径来跟踪和报告错误链。其工作原理是返回 Option<&(dyn Error + 'static)> 类型,表明在当前错误发生时可能触发它的底层错误("原因"或"源头"),或者如果没有更深层的错误则返回 None

标准库(std)中一个常见的 std::error::Error trait source 方法的使用示例是处理文件 I/O 操作中可能出现的错误。例如,尝试打开一个不存在的文件会返回 std::io::Error,我们可以使用 source 方法来获取错误的底层来源。以下是一个例子:

rust 复制代码
use std::error::Error;
use std::fs::File;
use std::io;

fn open_file(file_path: &str) -> Result<File, io::Error> {
    File::open(file_path)
}

fn main() {
    match open_file("non_existent_file.txt") {
        Ok(_) => println!("File opened successfully."),
        Err(e) => {
            println!("An error occurred: {}", e);
            if let Some(source) = e.source() {
                println!("Underlying error: {}", source);
            }
        }
    }
}

在这个例子中,open_file 函数尝试打开一个不存在的文件,返回了一个 std::io::Error 类型的错误。然后我们在主函数中检查这个错误,首先打印基本的错误消息,随后是源错误(source error),也就是导致这个错误的底层系统错误。在 Unix 系统中,可能是因为 "No such file or directory"(没有这样的文件或目录)。在这种情况下,源错误(source)提供了额外的有关底层操作系统错误的信息,source 方法让我们能够访问到这个原始错误。

使用 anyhow 简化应用程序的错误处理

anyhow 是一个 Rust 库,旨在提供灵活的错误处理机制,特别适用于应用程序开发,其中可能需要处理多种错误类型,但不需要编写大量的自定义错误代码。该库的核心是 anyhow::Error 类型,它是一个通用的动态错误类型,能够封装任何实现了 std::error::Error trait 的错误类型。

以下是 anyhow 的一些主要特点:

  1. 简化的错误创建和返回 : 使用 anyhow,你可以通过 anyhow! 宏轻松创建错误。它可以接受字符串、字符串字面量,甚至格式化的错误消息。同时,它支持 ? 运算符用于错误的传递。
  2. 动态错误类型 : anyhow 提供的错误类型是动态的,可以封装任意的错误类型。这允许你在不需要详细错误分类或向上游库用户暴露错误类型定义时,快速构建应用程序。
  3. 链式错误 : anyhow 支持"链式"错误,这意味着你可以将一个错误附加到另一个错误上,形成一个错误链。这在诊断错误原因上特别有用。
  4. 无需定义自定义错误类型 : 对于许多应用场景,特别是应用程序开发,不需要定义和维护一整套自己的错误类型。与其他错误处理库(如 thiserror)不同,anyhow 提供了无需自定义错误类型即可进行错误处理的功能。
  5. Backtrace 支持 : anyhow 自动捕获错误发生时的回溯(如果 Rust 已经开启了回溯捕获),这对调试和错误报告至关重要。

一个典型的 anyhow 使用例子如下:

rust 复制代码
use anyhow::{anyhow, Result};

fn may_fail(flag: bool) -> Result<()> {
    if flag {
        Ok(())
    } else {
        Err(anyhow!("An error occurred"))
    }
}

fn main() -> Result<()> {
    may_fail(false)?;
    Ok(())
}

使用 thiserror 简化库中错误的定义和实现

thiserror 是 Rust 中的一个库,旨在简化错误的定义和实现,特别是对 crate 作者在定义需要向使用者公开的自定义错误类型时非常有用。与 anyhow 库相比,thiserror 更适用于库开发场景,它允许创建结构化错误,并为每种错误自动实现了 std::error::Error trait。

以下是 thiserror 的一些主要特点:

  1. 结构化错误定义 : thiserror 提供了一个简洁的宏,用于定义包含错误同一信息和错误原因的错误枚举和结构。
  2. 自动实现 : Error trait 和相关方法(如 source()fmt())的实现会通过 thiserror 宏自动完成,避免了编写样板代码。
  3. 良好的兼容性 : 所定义的错误类型天然支持 Rust 中的错误处理模式,例如使用 ? 运算符将错误向上传递。
  4. 便于使用 : 通过 thiserror 定义的错误可以无缝地集成到库用户的错误处理流程中,同时还保持了 Rust 类型系统带来的类型安全和模式匹配的优势。

一个使用 thiserror 的例子如下:

rust 复制代码
use thiserror::Error;

// 自定义错误枚举
#[derive(Error, Debug)]
pub enum MyError {
    #[error("the data for the key `{0}` is not available")]
    NotFound(String),
    #[error("the data for the key `{0}` is malformed")]
    MalformedData(String),
    #[error(transparent)]
    Io(#[from] std::io::Error), // 表示从 std::io::Error 转换
    // 其他错误变体...
}

fn use_my_error() -> Result<(), MyError> {
    // 函数逻辑...
    Err(MyError::NotFound("key".to_string()))
}

fn main() {
    match use_my_error() {
        Ok(_) => println!("Success!"),
        Err(e) => println!("An error occurred: {}", e),
    }
}

参考资料

相关推荐
一个小坑货22 分钟前
Cargo Rust 的包管理器
开发语言·后端·rust
bluebonnet2726 分钟前
【Rust练习】22.HashMap
开发语言·后端·rust
VertexGeek3 小时前
Rust学习(八):异常处理和宏编程:
学习·算法·rust
前端与小赵1 天前
什么是Sass,有什么特点
前端·rust·sass
一个小坑货1 天前
Rust基础
开发语言·后端·rust
Object~1 天前
【第九课】Rust中泛型和特质
开发语言·后端·rust
码农飞飞1 天前
详解Rust结构体struct用法
开发语言·数据结构·后端·rust·成员函数·方法·结构体
PW2 天前
JavaScript基础实践:电话号码格式化的多种实现方式
javascript·代码规范
Kisorge2 天前
【C语言】C语言代码的编写规范、注释规范
java·c语言·代码规范
Dontla2 天前
Rust derive macro(Rust #[derive])Rust派生宏
开发语言·后端·rust