外观
雷区 5:String 当错误用,追查无门
症状
rust
use std::fs;
fn load_config() -> Result<(), String> {
let content = fs::read_to_string("config.txt")
.map_err(|e| format!("读文件失败:{}", e))?;
for (line, text) in content.lines().enumerate() {
if !text.contains('=') {
return Err(format!("第 {} 行缺少等号", line + 1));
}
}
Ok(())
}
fn main() {
match load_config() {
Ok(()) => println!("配置加载成功"),
Err(message) => println!("出错了:{}", message),
}
}运行起来一切正常——输出也不错。问题在代码结构上:String 当错误,调用方除了"打印",什么都干不了。
诊断
用 String 当错误类型是新手最常见的选择(我们的书前几章也这么干过),它的三个致命伤:
- 调用方无法"程序化"处理——只能整句打印,没法区分"文件不存在"和"格式错误",没法决定"要不要重试、要不要换个路径"
- 错误信息到处拼——每个
map_err拼一次,格式全靠自觉,漏拼、拼错、重复,防不胜防 - 类型不表达——
Result<_, String>读不出来"可能出什么错",编译器帮不上忙,文档帮不上忙
对比:自定义错误类型是结构化的——调用方用 match 按变体处理,编译器保证所有变体都被覆盖(第 5 章穷尽性)。
解药
解药:定义自己的错误枚举,实现 Display:
rust
use std::fmt;
#[derive(Debug)]
enum AppError {
FileNotFound(String),
ParseFailed { line: u32, reason: String },
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
AppError::FileNotFound(path) => write!(f, "找不到文件:{}", path),
AppError::ParseFailed { line, reason } => {
write!(f, "第 {} 行解析失败:{}", line, reason)
}
}
}
}
fn load_config() -> Result<(), AppError> {
let content = std::fs::read_to_string("config.txt")
.map_err(|_| AppError::FileNotFound("config.txt".to_string()))?;
for (line, text) in content.lines().enumerate() {
if !text.contains('=') {
return Err(AppError::ParseFailed {
line: line as u32 + 1,
reason: "缺少等号".to_string(),
});
}
}
Ok(())
}
fn main() {
match load_config() {
Ok(()) => println!("配置加载成功"),
Err(error) => println!("出错了:{}", error),
}
}现在调用方可以按变体处理:
rust
match load_config() {
Err(AppError::FileNotFound(_)) => println!("给用户一个'怎么创建配置'的指引"),
Err(AppError::ParseFailed { line, .. }) => println!("用编辑器打开 config.txt,看第 {} 行", line),
_ => {}
}不想手写 Display?
Display 的样板代码用 thiserror(雷区 7)自动生成——真实项目的标准做法。
预防
- 判断标准:这个错误,调用方需要"区分"吗? 只需要打印 → String 勉强够;需要区分处理 → 必须枚举
- 错误类型的字段也要有信息:
ParseFailed { line, reason }比ParseFailed(String)强——行号、原因分开,调用方各取所需 - 命名习惯:
XxxError(如AppError/ConfigError),实现Debug+Display(debug 给开发者,display 给用户) - 库的公共 API 绝不用 String 当错误(用户没法处理);应用内部可以先用 String 顶着,长成库再升级