功能说明
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(_))));
}