← 中文资源

简体中文版本

Rust 自定义错误枚举与错误源

用标准库实现配置错误枚举、Display、Error::source 与 From 转换,并用问号运算符保留底层解析错误。

阅读英文原文

功能说明

ConfigError 覆盖缺失键、格式错误、端口解析失败、端口越界和 IO 错误。parse_config 逐行读取 key=value,跳过空行与注释,规范化键值后构造 Config;load 演示如何用问号运算符把 std::fs 的 IO 错误自动转换为 ConfigError::Io。main 分别验证成功配置、缺端口、端口溢出和文件不存在四种情况。

关键代码

Display 为每种变体提供面向用户的描述,Error::source 只在确实存在底层错误时返回引用,形成可遍历的错误链。InvalidPort 保存原始的 ParseIntError,而不是提前拼成字符串,因此调用方仍能检查错误类型。Fromio::Error 让 load 中的问号可用。端口先按 u16 解析,零值再单独判为 OutOfRange,超过 65535 则由解析器报 InvalidPort。

使用注意

手写错误枚举适合库或需要稳定错误分类的场景;应用层可以改用 thiserror 减少样板代码。不要用字符串匹配来区分错误,应匹配枚举变体。source 返回的引用要求底层错误拥有 ’static 生命周期。新增变体可能破坏下游穷尽匹配,公开 API 可加 non_exhaustive。Malformed 同时承载语法错误与未知键,若调用方需要不同处理应拆分变体。

验证方式

运行 cargo run,成功配置打印 host 与 port;缺端口时报 missing required key port;70000 报 invalid port 并显示 caused by 的解析原因;zero 同样进入 InvalidPort 分支;不存在的文件被识别为 Io 变体。可为每种变体写单元测试,断言 Display 文案与 source 是否存在,并验证 load 对临时文件内容的解析结果。

代码示例

use std::error::Error;
use std::fmt;
use std::io;
use std::num::ParseIntError;

#[derive(Debug)]
enum ConfigError {
    Missing(&'static str),
    Malformed { detail: String },
    InvalidPort { value: String, source: ParseIntError },
    OutOfRange(u16),
    Io(io::Error),
}

impl fmt::Display for ConfigError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ConfigError::Missing(key) => write!(f, "missing required key `{key}`"),
            ConfigError::Malformed { detail } => write!(f, "malformed config: {detail}"),
            ConfigError::InvalidPort { value, .. } => write!(f, "invalid port `{value}`"),
            ConfigError::OutOfRange(port) => write!(f, "port {port} must be between 1 and 65535"),
            ConfigError::Io(err) => write!(f, "io error: {err}"),
        }
    }
}

impl Error for ConfigError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            ConfigError::InvalidPort { source, .. } => Some(source),
            ConfigError::Io(err) => Some(err),
            ConfigError::Missing(_) | ConfigError::Malformed { .. } | ConfigError::OutOfRange(_) => None,
        }
    }
}

impl From<io::Error> for ConfigError {
    fn from(err: io::Error) -> Self {
        ConfigError::Io(err)
    }
}

#[derive(Debug, PartialEq)]
struct Config {
    host: String,
    port: u16,
}

fn parse_config(raw: &str) -> Result<Config, ConfigError> {
    let mut host = None;
    let mut port = None;

    for raw_line in raw.lines() {
        let line = raw_line.trim();
        if line.is_empty() || line.starts_with('#') {
            continue;
        }
        let (key, value) = line
            .split_once('=')
            .ok_or_else(|| ConfigError::Malformed { detail: format!("expected key=value, got `{line}`") })?;
        let key = key.trim();
        let value = value.trim();

        match key {
            "host" => {
                if value.is_empty() {
                    return Err(ConfigError::Missing("host"));
                }
                host = Some(value.to_string());
            }
            "port" => {
                let parsed = value
                    .parse::<u16>()
                    .map_err(|source| ConfigError::InvalidPort { value: value.to_string(), source })?;
                if parsed == 0 {
                    return Err(ConfigError::OutOfRange(parsed));
                }
                port = Some(parsed);
            }
            other => {
                return Err(ConfigError::Malformed { detail: format!("unsupported key `{other}`") });
            }
        }
    }

    Ok(Config {
        host: host.ok_or(ConfigError::Missing("host"))?,
        port: port.ok_or(ConfigError::Missing("port"))?,
    })
}

fn load(path: &str) -> Result<Config, ConfigError> {
    let raw = std::fs::read_to_string(path)?;
    parse_config(&raw)
}

fn main() {
    let valid = "host = 127.0.0.1\nport = 8080\n";
    println!("valid: {:?}", parse_config(valid));

    for raw in [
        "port = 8080\n",
        "host = localhost\nport = 70000\n",
        "host = localhost\nport = zero\n",
    ] {
        match parse_config(raw) {
            Ok(config) => println!("unexpected success: {config:?}"),
            Err(err) => {
                println!("error: {err}");
                if let Some(source) = err.source() {
                    println!("  caused by: {source}");
                }
            }
        }
    }

    let missing = load("does-not-exist.conf");
    println!("missing file became io error: {}", matches!(missing, Err(ConfigError::Io(_))));
}