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 转换并支持上下文附加。
相关推荐
guyoung1 小时前
BoxAgnts 工具系统(4)——Tool Trait 与并发上下文模型
rust·agent·ai编程
xuhaoyu_cpp_java2 小时前
项目学习(三)代码生成器
java·经验分享·笔记·学习
my_daling2 小时前
松下伺服驱动器参数保存流程(已在松下A5上验证)
笔记
worilb2 小时前
Spring Cloud 学习与实践(8):Spring Cloud Gateway 统一入口、路由转发与双重跨域故障演练
学习·spring·spring cloud
初圣魔门首席弟子2 小时前
学习工作方法论与任务执行计划
学习
智者知已应修善业3 小时前
【51单片机初始化D5-D8亮,每按键按下D1到D4全亮,再按下恢复,如此循环】2024-3-26
c++·经验分享·笔记·算法·51单片机
skywalk81633 小时前
记录段言的开发过程
开发语言·学习·编程
知识分享小能手3 小时前
Hadoop学习教程,从入门到精通, MapReduce分布式计算框架 — 完整知识点与代码案例(4)
hadoop·学习·mapreduce
YM52e3 小时前
鸿蒙HarmonyOS ArkTS 实战:教师座椅出入记录 APP 从零到一
学习·华为·harmonyos·鸿蒙系统