rust语言学习笔记Trait(十六)Error(错误)

Rust 的 std::error::Error trait 是错误处理生态的基石。所有可被当作"错误"使用的类型都需要实现这个 trait,它让不同来源的错误能统一传播、展示和追溯。

1、Error trait 定义

rust 复制代码
pub trait Error: Debug + Display {
    fn source(&self) -> Option<&(dyn Error + 'static)> { None }
    fn description(&self) -> &str { ... }           // 已弃用,直接使用 Display
    fn cause(&self) -> Option<&dyn Error> { ... }   // 已弃用,被 source() 取代
    fn provide<'a>(&'a self, request: &mut Request<'a>) { ... }  // 实验性方法,极少用到
}

fn source(&self) -> Option<&(dyn Error + 'static)>

  • 默认返回 None,表示当前错误没有更底层的来源。

  • 当一个高阶模块的错误是由底层模块的错误引起时,我们会重写这个方法,把底层的错误返回出去。这样外部代码就可以通过不断调用 source() 来‌遍历整个错误链‌,一直追溯到最根本的原因。

  • 例如:你的自定义错误包含一个 std::io::Error,那么 source() 就应该返回这个 io::Error 的引用。

  • 这是‌错误溯源(error chaining)的关键‌,也是 Rust 错误处理优雅的地方。

  • 任何实现 Error 的类型都必须同时实现 Debug 和 Display‌;
    • Debug:面向开发者,输出调试细节
    • Display:面向用户,给出小写、无标点的简洁描述,例如 "字符串中发现无效数字"
  • 如果你的错误包装了其他错误,就重写 source() 来返回它;
  • 忽略 description()cause(),它们只是过时的历史;
  • provide() 则留待未来特性稳定后再使用。

2、为什么需要自定义错误类型

直接用 StringBox<dyn Error> 虽然方便,但会丢失结构化信息,导致:

  • 无法区分错误类别(IO 错误、解析错误、业务错误)
  • 调用方无法按类型进行恢复或分支处理
  • 不能携带上下文数据(如文件名、行号)
  • 不利于日志收集和测试验证

因此,实际项目通常会定义‌自定义错误枚举或结构体 ‌,并为其实现 Error trait,同时配合 From 转换以利用 ? 操作符自动传播错误。

3、手动实现 Error trait

(1)自定义错误

rust 复制代码
use std::fmt;

// 自定义错误枚举
#[derive(Debug)]
enum MyError {
    ErrA(String),
    ErrB(String),
}

/// 实现 fmt::Display trait
/// 将错误转换为可读的字符串表示
impl fmt::Display for MyError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            MyError::ErrA(msg) => write!(f, "ErrA错误: {}!", msg),
            MyError::ErrB(msg) => write!(f, "ErrB错误: {}!", msg),
        }
    }
}

/// 成功返回 i32,失败返回 MyError
fn my_fn(i: i32) -> Result<i32, MyError> {
    match i {
        1 => Err(MyError::ErrA("不能是1".to_string())),
        2 => Err(MyError::ErrB("不能是2".to_string())),
        _ => Ok(i),
    }
}

fn main() {
    match my_fn(1) {
        Ok(i) => println!("成功: {}", i),
        Err(e) => eprintln!("失败: {}", e),   // 失败: ErrA错误: 不能是1!
    }
   
    match my_fn(2) {
        Ok(i) => println!("成功: {}", i),
        Err(e) => eprintln!("失败: {}", e),   // 失败: ErrB错误: 不能是2!
    }
  
    match my_fn(3) {
        Ok(i) => println!("成功: {}", i),    // 成功: 3
        Err(e) => eprintln!("失败: {}", e),
    }
    
    println!("{:?}", my_fn(2));    // Err(ErrB("不能是2"))
    println!("{:?}", my_fn(3));    // Ok(3)
}

(2)自定义错误装箱为 Box<dyn Error>

rust 复制代码
use std::fmt;

// 自定义错误枚举
#[derive(Debug)]
enum MyError {
    ErrA(String),
    ErrB(String),
}

/// 实现 fmt::Display trait
/// 将错误转换为可读的字符串表示
impl fmt::Display for MyError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            MyError::ErrA(msg) => write!(f, "ErrA错误: {}!", msg),
            MyError::ErrB(msg) => write!(f, "ErrB错误: {}!", msg),
        }
    }
}

/// MyError 实现 std::error::Error trait
/// 这使得 MyError 可以作为通用错误类型使用(通过 Box<dyn Error>)
impl Error for MyError {}

/// 返回值: Result<i32, Box<dyn Error>> - 成功返回 i32,失败返回装箱的错误对象
fn my_fn(i: i32) -> Result<i32, Box<dyn Error>> {
    match i {
        1 => Err(Box::new(MyError::ErrA("不能是1".to_string()))),
        2 => Err(Box::new(MyError::ErrB("不能是2".to_string()))),
        _ => Ok(i),
    }
}

fn main() {

    match my_fn(1) {
        Ok(i) => println!("成功: {}", i),
        Err(e) => eprintln!("失败: {}", e),  // 失败: ErrA错误: 不能是1!
    }

    match my_fn(2) {
        Ok(i) => println!("成功: {}", i),
        Err(e) => eprintln!("失败: {}", e), // 失败: ErrB错误: 不能是2!
    }
    
    match my_fn(3) {
        Ok(i) => println!("成功: {}", i),   // 成功: 3
        Err(e) => eprintln!("失败: {}", e), 
    }

    
    println!("{:?}", my_fn(2));    // Err(ErrB("不能是2"))
    println!("{:?}", my_fn(3));    // Ok(3)
}

(3)实现底层错误传递 source()

rust 复制代码
use std::fmt;
use std::error::Error;

// --- 1. 定义底层错误 (Custom Error 2) ---
#[derive(Debug)]
struct ErrorB {
    msg: String,
}

impl fmt::Display for ErrorB {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "底层错误 B: {}", self.msg)
    }
}

// ErrorB 没有更底层的错误,所以 source() 使用默认实现 (返回 None)
impl Error for ErrorB {}


// --- 2. 定义上层错误 (Custom Error 1),它包含 ErrorB ---
#[derive(Debug)]
struct ErrorA {
    context: String,
    source_error: ErrorB, // 持有底层错误
}

impl fmt::Display for ErrorA {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "上层错误 A : {}", self.context)
    }
}

impl Error for ErrorA {
    // 关键:实现 source() 方法,返回对内部 ErrorB 的引用
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        Some(&self.source_error)
    }
}

fn main() {
    // 模拟产生一个底层错误 B
    let err_b = ErrorB {
        msg: "B数据库连接超时".to_string(),
    };

    // 将 B 包装进上层错误 A
    let err_a = ErrorA {
        context: "A用户登录失败".to_string(),
        source_error: err_b,
    };

    // 打印上层错误A
    println!("直接打印 ErrorA: {}", err_a);
    
    // 通过 source() 链式获取底层错误
    if let Some(source) = err_a.source() {
        println!("通过 source() 获取到的底层错误: {}", source);
        
        // 验证底层错误是否还有更底层的错误
        if let Some(deep_source) = source.source() {
            println!("更深层的错误: {}", deep_source);
        } else {
            println!("底层错误已无更多来源.");
        }
    }

    // 循环获取所有底层错误,不显示上层错误
    let mut source = err_a.source();                 // 从上层错误 A 开始
    while let Some(e) =source  {                     // 循环直到 source() 返回 None
        println!("{}", e);                           // 打印当前错误
        source = e.source();                         // 递归获取更底层的错误
    }
}

程序输出:

bash 复制代码
直接打印 ErrorA: 上层错误 A : A用户登录失败

通过 source() 获取到的底层错误: 底层错误 B: B数据库连接超时
底层错误已无更多来源.

底层错误 B: B数据库连接超时

4、实现 From 自动转换(方便使用 ?

rust 复制代码
#[derive(Debug)]
struct ErrorB {
    msgb: String,
}

#[derive(Debug)]
struct ErrorA {
    msga: String,
}

// 将 B 错误包装成 A 错误
impl From<ErrorB> for ErrorA {
    fn from(err: ErrorB) -> Self {
            ErrorA{
                msga: err.msgb,
            }
    }
}

// 使用 ? 转换错误
fn test_error_a()-> Result<(), ErrorA> {
    Err(ErrorB {msgb: "B发生错误".to_string()})?
}

fn main() {
    let e = test_error_a();
    println!("{:?}", e);
}

// 程序输出:Err(ErrorA { msga: "B发生错误" })

5、错误处理库

6、总结

  • (1)用枚举区分错误种类‌:让调用方能进行模式匹配并采取不同恢复策略。
  • (2)实现 source() 构建错误链‌:保留根因,方便调试和日志追踪。
  • (3)**配合 From?**‌:减少显式 map_err,让代码更流畅。
  • (4)thiserror 降低样板代码‌:在生产项目中广泛使用,安全且高效。
  • (5)**不随意使用 unwrapexpect**‌:除非在测试代码或确定不会失败的场景(如配置加载失败直接 panic),否则应始终通过 Result 传播错误。
  • ‌(6)避免在公共 API 中暴露内部实现细节‌:错误消息应面向用户或调用者,而不是泄露内部路径或变量名,除非用于调试日志。
  • (7)考虑使用 anyhow 处理应用层错误 ‌:对于不需要精细分类的应用程序顶层错误(如 main 函数或 CLI 工具),anyhow::Error 提供了极大的便利性,它自动实现了 From 转换并支持上下文附加。
相关推荐
独孤留白20 小时前
从C到Rust:Rust 的 Trait 不是Interface,那是什么?
rust
花褪残红青杏小1 天前
Rust图像处理第7节-马赛克像素化:分块取平均色实现打码风格
rust·webassembly·图形学
doiito2 天前
【Agent Harness】Gliding Horse 设计细节 -- 不跟风开发自己的AI Agent
架构·rust·agent
doiito2 天前
【Agent Harness】Gliding Horse 核心设计理念,不跟风开发自己的AI Agent
ai·rust·架构设计·系统设计·ai agent
花褪残红青杏小2 天前
Rust图像处理第6节- 均值模糊 & 中值模糊:3×3 邻域的两种经典玩法
rust·webassembly·图形学
子兮曰3 天前
前端工具链的「Rust 化」:一场没有赢家的军备竞赛?
前端·后端·rust
星栈3 天前
写 Dioxus Demo 不难,难的是把它写成项目
前端·rust·前端框架
mCell3 天前
【锐评】桌面端技术营销:别拿跑分当工程判断
前端·rust·electron
武子康3 天前
调查研究-201 Rust 里的 dev build 和 release build:为什么同一份代码性能差这么多?
后端·架构·rust
RainCity3 天前
Java Swing 自定义组件库分享(十二)
java·笔记·后端