6.3 自定义错误类型:案由体系


6.3 自定义错误类型:案由体系

本节摘要:库应当暴露具体、可匹配的错误枚举,而不是字符串或万能箱。本节用"档案库错误"贯穿:定义错误枚举、实现 Display 让它可读、实现 From 让 ? 自动换乘、对外以统一类型收口。读完你能为库或模块设计一套完整的错误体系。

一个完整的错误体系

use std::fmt; #[derive(Debug)] enum ArchiveError { NotFound(String), PermissionDenied { user: String }, Corrupted(u32), // 校验码 Io(std::io::Error), // 包裹底层错误 } impl fmt::Display for ArchiveError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { ArchiveError::NotFound(name) => write!(f, "档案不存在:{}", name), ArchiveError::PermissionDenied { user } => write!(f, "无权访问:{}", user), ArchiveError::Corrupted(code) => write!(f, "数据损坏,校验码 {}", code), ArchiveError::Io(e) => write!(f, "底层 IO 失败:{}", e), } } } impl std::error::Error for ArchiveError {} impl From<std::io::Error> for ArchiveError { fn from(e: std::io::Error) -> Self { ArchiveError::Io(e) } }

三个履约各司其职:Display 面向人,Error 契约接入标准库生态(可当 trait 对象传递),From 让函数里 File::open(...)? 的 io 错误自动换乘成 ArchiveError——6.1 节的承诺在此兑现。

调用方的收益:按案由分流

match archive.load("173号") { Ok(data) => process(data), Err(ArchiveError::NotFound(name)) => retry_create(name), Err(ArchiveError::PermissionDenied { .. }) => escalate(), Err(e) => log_fatal(e), }

错误是枚举,分支即处理策略——字符串错误给不了这种判决力。生态里的派生宏工具(如 thiserror 一类)能把上面四段样板压缩成几行注解,原理即第 9 章的过程宏:手写一遍样板再上工具,才知道工具生成了什么。

要点回顾

  • 错误枚举一个变体一种案由,携带有用的载荷而非泛化字符串;
  • Display 管可读、Error 管互操作、From 管自动换乘,三件套是完整履约;
  • 库对外的函数签名收口到统一错误类型,内部再杂也不外泄;
  • 派生宏工具是样板加速器,语义与手写完全一致。

错误分层:一个解析器的完整判例

use std::fmt; use std::num::ParseIntError; #[derive(Debug)] enum CaseError { EmptyInput, BadNumber(ParseIntError), // 内层错误原样封存 BadLength { got: usize, want: usize }, } impl fmt::Display for CaseError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { CaseError::EmptyInput => write!(f, "输入为空"), CaseError::BadNumber(e) => write!(f, "编号解析失败:{}", e), CaseError::BadLength { got, want } => write!(f, "行长度 {},要求 {}", got, want), } } } impl std::error::Error for CaseError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { CaseError::BadNumber(e) => Some(e), // 暴露错误链供上层勘查 _ => None, } } } impl From<ParseIntError> for CaseError { fn from(e: ParseIntError) -> Self { CaseError::BadNumber(e) } } // 有了 From,? 自动转换 fn parse_line(line: &str) -> Result<u32, CaseError> { let t = line.trim(); if t.is_empty() { return Err(CaseError::EmptyInput); } let n: u32 = t.parse()?; // ParseIntError 经 From 静默转换 if n == 0 { return Err(CaseError::BadLength { got: 0, want: 1 }); } Ok(n) } fn main() { println!("{:?}", parse_line(" 17 ")); println!("{}", parse_line("甲").unwrap_err()); }

这个骨架的四个部件各有职能:enum 定形态、Display 给人读、Error 给机器读(source 链)、From 给 ? 铺路。库的公开错误类型照此手写或用 thiserror 宏等价生成;应用的顶层用 anyhow 风格的 Box<dyn Error> 即可,两者边界是"是否要被下游程序化处理"。

错误链的读取程序

fn print_chain(mut e: &dyn std::error::Error) { let mut i = 0; loop { println!(" {}: {}", i, e); match e.source() { Some(s) => { e = s; i += 1; } None => break, } } }

排查线上问题时,只看最外层 Display 常常不够——真正的案发第一现场往往在链条第二、三层。日志中间件调用 source 链把整条因果打全,是错误处理的最后一块拼图。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U