bigywater.tech
Rust · 73

自定义错误类型

写一个能让调用方好好处理的错误类型,需要实现哪些 trait?source 链怎么用?Box 与 downcast 怎么配合?错误太大会怎样?这篇用一个会读配置文件的例子,把这些一次跑通。

4 项
本文的错误类型实现的内容:Debug、Display、Error、From
129 → 132
Big 错误枚举与 Result 的字节数
16 字节
Result> 的大小

完整的错误类型

实测 · 本机执行输出
  源码:
      1 | use std::{error::Error, fmt, num::ParseIntError, fs, io};
      2 | #[derive(Debug)]
      3 | enum ConfigError { Io(io::Error), Parse { key: String, source: ParseIntError }, Missing(&'static str) }
      4 | impl fmt::Display for ConfigError {
      5 |     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
      6 |         match self { ConfigError::Io(_) => write!(f, "读取配置文件失败"), ConfigError::Parse { key, .. } => write!(f, "配置项 {} 不是有效整数", key), ConfigError::Missing(k) => write!(f, "缺少配置项 {}", k) }
      7 |     }
      8 | }
      9 | impl Error for ConfigError {
     10 |     fn source(&self) -> Option<&(dyn Error + 'static)> {
     11 |         match self { ConfigError::Io(e) => Some(e), ConfigError::Parse { source, .. } => Some(source), ConfigError::Missing(_) => None }
     12 |     }
     13 | }
     14 | impl From<io::Error> for ConfigError { fn from(e: io::Error) -> Self { ConfigError::Io(e) } }
     15 | fn load(path: &str, key: &'static str) -> Result<i32, ConfigError> {
     16 |     let text = fs::read_to_string(path)?;
     17 |     let line = text.lines().find(|l| l.starts_with(key)).ok_or(ConfigError::Missing(key))?;
     18 |     let v = line.split('=').nth(1).unwrap_or("").trim();
     19 |     v.parse().map_err(|e| ConfigError::Parse { key: key.to_string(), source: e })
     20 | }
     21 | fn report(e: &dyn Error) { println!("错误: {}", e); let mut cur = e.source(); let mut d = 1; while let Some(s) = cur { println!("{}由 {} 引起", "  ".repeat(d), s); cur = s.source(); d += 1; } }
     22 | fn main() {
     23 |     let dir = std::env::temp_dir().join("u73cfg"); fs::create_dir_all(&dir).unwrap();
     24 |     let p = dir.join("a.conf"); fs::write(&p, "port = 8080\nworkers = many\n").unwrap();
     25 |     let ps = p.to_str().unwrap();
     26 |     println!("port -> {:?}", load(ps, "port"));
     27 |     for (path, key) in [(ps, "workers"), (ps, "timeout"), ("/不存在的/文件", "port")] {
     28 |         match load(path, key) { Ok(v) => println!("{} -> {}", key, v), Err(e) => { report(&e); println!("  Debug: {:?}", e); } }
     29 |     }
     30 |     let _ = fs::remove_dir_all(&dir);
     31 | }
  编译:成功
  运行(退出码 0):
    port -> Ok(8080)
    错误: 配置项 workers 不是有效整数
      由 invalid digit found in string 引起
      Debug: Parse { key: "workers", source: ParseIntError { kind: InvalidDigit } }
    错误: 缺少配置项 timeout
      Debug: Missing("timeout")
    错误: 读取配置文件失败
      由 No such file or directory (os error 2) 引起
      Debug: Io(Os { code: 2, kind: NotFound, message: "No such file or directory" })

? 的转换规则见Result 错误传播。

缺少 Display

实测 · 本机执行输出
  源码:
      1 | use std::error::Error;
      2 | #[derive(Debug)] struct MyErr;
      3 | impl Error for MyErr {}
      4 | fn main() { let e: Box<dyn Error> = Box::new(MyErr); println!("{:?}", e); }
  编译:失败
    error[E0277]: `MyErr` doesn't implement `std::fmt::Display`
      --> <实验目录>/src/u73b.rs:3:16
       |
     3 | impl Error for MyErr {}
       |                ^^^^^ unsatisfied trait bound
       |
    help: the trait `std::fmt::Display` is not implemented for `MyErr`
      --> <实验目录>/src/u73b.rs:2:18
       |
     2 | #[derive(Debug)] struct MyErr;
       |                  ^^^^^^^^^^^^
    note: required by a bound in `std::error::Error`
      --> <rust 标准库源码>/library/core/src/error.rs:59:26
       |
    59 | pub trait Error: Debug + Display {
       |                          ^^^^^^^ required by this bound in `Error`
    
    error: aborting due to 1 previous error
    
    For more information about this error, try `rustc --explain E0277`.

Box 与 downcast

实测 · 本机执行输出
  编译:成功
  运行(退出码 0):
    k=0: 是 Quota,内部值 100
    k=1: 是 ParseIntError: invalid digit found in string
    size_of::<Box<dyn Error>>() = 16 字节(胖指针)

错误的大小

实测 · 本机执行输出
  编译:成功
  运行(退出码 0):
    Small 2  WithString 24  Big 129
    Result<u32, Small> 8  Result<u32, WithString> 24  Result<u32, Big> 132  Result<u32, Box<Big>> 16
    io::Error 8  ParseIntError 1  Box<dyn Error> 16

thiserror 与 anyhow 把这些样板代码自动化,见thiserror 与 anyhow 的边界。

分步演示

自定义错误
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / rustc 1.98.1(brew)/ cargo 1.98.1;单个文件用 rustc 直接编译,需要依赖时用 cargo(经代理从 crates.io 下载);计时用 std::time::Instant,数字每次运行会有小幅抖动。配置文件写在系统临时目录并在结束后删除
    • 没有验证:backtrace 的捕获、Error::description 与 cause 的废弃状态、error provide API(不稳定)、跨线程传递错误所需的 Send + Sync 约束
    京ICP备2026067502号-1京公网安备11010502063487号