标准库与错误处理 intermediate 30 分钟 更新 2026-06-15

自定义错误类型

用枚举定义错误,实现 Display 和 Error trait。

自定义错误类型

你会学到什么

  • 用枚举把领域内的多种失败原因建模成一个错误类型。
  • 实现 Display 提供人类可读的信息,实现 std::error::Error 接入生态。
  • map_err 把底层错误转换成自己的错误。

最小示例

#[derive(Debug)]
enum ParseError {
    Empty,
    NotANumber(String),
}

运行代码

cd examples
cargo run -p rt_15_custom_errors
cargo test -p rt_15_custom_errors

代码讲解

实现 Display 后,错误可以被友好地打印;实现空的 Error trait 让它能放进 Box<dyn Error>

impl std::fmt::Display for ParseError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ParseError::Empty => write!(f, "输入为空"),
            ParseError::NotANumber(token) => write!(f, "不是数字: {token}"),
        }
    }
}

impl std::error::Error for ParseError {}

map_err 把标准库错误转换成自己的语义错误:

let value: i64 = trimmed
    .parse()
    .map_err(|_| ParseError::NotANumber(trimmed.to_string()))?;

常见错误

#[derive(Debug)] 而不实现 Display,错误就只能用 {:?} 打印,对用户不友好。生产代码两者通常都要。

实际项目中常用 thiserror 自动派生这些实现,我们会在“生态库”阶段介绍。

练习

  • ParseError 加一个 TooLarge(i64) 变体并在解析时使用。
  • 实现 From<std::num::ParseIntError> for ParseError,然后用 ? 替换 map_err

小结

自定义错误类型让失败原因变得清晰、可匹配。Display + Error 是接入 Rust 错误生态的标准组合。

下一步

接下来进入泛型与 trait 阶段,学习写出可复用的抽象代码。

完整示例代码

下面是 examples/15_custom_errors/ 的完整源码。无需 clone 仓库,直接在页面上阅读、复制、对照运行。

examples/15_custom_errors/src/main.rs
//! 自定义错误类型:实现 Display、Error 和 From。

use std::fmt;

#[derive(Debug, PartialEq)]
enum ParseError {
    Empty,
    NotANumber(String),
    NotPositive(i64),
}

impl fmt::Display for ParseError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ParseError::Empty => write!(formatter, "输入为空"),
            ParseError::NotANumber(token) => write!(formatter, "不是数字: {token}"),
            ParseError::NotPositive(value) => write!(formatter, "必须为正数: {value}"),
        }
    }
}

impl std::error::Error for ParseError {}

/// 解析一个正整数,失败时返回带语义的自定义错误。
fn parse_positive(input: &str) -> Result<u32, ParseError> {
    let trimmed = input.trim();
    if trimmed.is_empty() {
        return Err(ParseError::Empty);
    }
    let value: i64 = trimmed
        .parse()
        .map_err(|_| ParseError::NotANumber(trimmed.to_string()))?;
    if value <= 0 {
        return Err(ParseError::NotPositive(value));
    }
    Ok(value as u32)
}

fn main() {
    for input in ["42", "", "abc", "-5"] {
        match parse_positive(input) {
            Ok(value) => println!("{input:?} => {value}"),
            Err(error) => println!("{input:?} => 错误: {error}"),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parses_positive() {
        assert_eq!(parse_positive("42"), Ok(42));
        assert_eq!(parse_positive("  7 "), Ok(7));
    }

    #[test]
    fn rejects_bad_input() {
        assert_eq!(parse_positive(""), Err(ParseError::Empty));
        assert_eq!(
            parse_positive("abc"),
            Err(ParseError::NotANumber("abc".to_string()))
        );
        assert_eq!(parse_positive("-5"), Err(ParseError::NotPositive(-5)));
    }

    #[test]
    fn error_displays_message() {
        assert_eq!(ParseError::Empty.to_string(), "输入为空");
    }
}
examples/15_custom_errors/Cargo.toml
[package]
name = "rt_15_custom_errors"
version.workspace = true
edition.workspace = true
publish.workspace = true