错误处理实战总结
> 综合运用 Result、自定义错误类型和最佳实践,构建健壮的错误处理系统。
错误处理最佳实践
1. 使用 Result 处理预期错误
rust
▶ Run// ✅ 推荐:使用 Result
fn parse_number(s: &str) -> Result<i32, std::num::ParseIntError> {
s.parse()
}
// ❌ 不推荐:直接 panic
fn parse_number_bad(s: &str) -> i32 {
s.parse().unwrap() // 无效输入会 panic
}2. 提供有意义的错误信息
rust
▶ Run// ✅ 推荐:使用 expect 提供上下文
let file = File::open("config.toml")
.expect("Failed to open config.toml");
// ✅ 更佳:使用 anyhow 添加上下文
use anyhow::Context;
let content = std::fs::read_to_string("config.toml")
.context("Failed to read configuration file")?;3. 不要忽略错误
rust
▶ Run// ❌ 错误:忽略错误
let _ = File::open("important.txt");
// ✅ 正确:至少记录错误
if let Err(e) = File::open("important.txt") {
eprintln!("警告:无法打开文件:{}", e);
}4. 尽早返回
rust
▶ Run// ❌ 不推荐:深层嵌套
fn process(data: &str) -> Result<(), Error> {
match validate(data) {
Ok(_) => {
match parse(data) {
Ok(parsed) => {
match execute(parsed) {
Ok(_) => Ok(()),
Err(e) => Err(e),
}
}
Err(e) => Err(e),
}
}
Err(e) => Err(e),
}
}
// ✅ 推荐:使用?尽早返回
fn process(data: &str) -> Result<(), Error> {
validate(data)?;
let parsed = parse(data)?;
execute(parsed)?;
Ok(())
}5. 收集多个错误
rust
▶ Run// 验证多个字段时,收集所有错误
#[derive(Debug)]
struct ValidationError {
field: String,
message: String,
}
fn validate_form(name: &str, email: &str, age: u8)
-> Result<(), Vec<ValidationError>>
{
let mut errors = Vec::new();
if name.is_empty() {
errors.push(ValidationError {
field: "name".to_string(),
message: "不能为空".to_string(),
});
}
if !email.contains('@') {
errors.push(ValidationError {
field: "email".to_string(),
message: "格式无效".to_string(),
});
}
if age < 18 {
errors.push(ValidationError {
field: "age".to_string(),
message: "必须年满 18 岁".to_string(),
});
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}完整示例
示例 1:配置文件解析器
rust
▶ Runuse std::fs;
use std::io;
use std::path::Path;
use thiserror::Error;
#[derive(Error, Debug)]
enum ConfigError {
#[error("无法读取配置文件:{0}")]
Io(#[from] io::Error),
#[error("无效的 JSON: {0}")]
Json(#[from] serde_json::Error),
#[error("缺少必需字段:{0}")]
MissingField(String),
#[error("无效的值:字段={field}, 原因={reason}")]
InvalidValue { field: String, reason: String },
}
#[derive(Debug)]
struct Config {
host: String,
port: u16,
debug: bool,
}
impl Config {
fn load(path: impl AsRef<Path>) -> Result<Self, ConfigError> {
// 读取文件
let content = fs::read_to_string(path)?;
// 解析 JSON
let json: serde_json::Value = serde_json::from_str(&content)?;
// 提取字段
let host = json.get("host")
.and_then(|v| v.as_str())
.ok_or_else(|| ConfigError::MissingField("host".to_string()))?
.to_string();
let port = json.get("port")
.and_then(|v| v.as_u64())
.ok_or_else(|| ConfigError::MissingField("port".to_string()))?;
if port > 65535 {
return Err(ConfigError::InvalidValue {
field: "port".to_string(),
reason: format!("端口号 {} 超出范围", port),
});
}
let debug = json.get("debug")
.and_then(|v| v.as_bool())
.unwrap_or(false);
Ok(Config { host, port: port as u16, debug })
}
}
fn main() -> Result<(), ConfigError> {
// 创建测试配置文件
std::fs::write("config.json", r#"{
"host": "localhost",
"port": 8080,
"debug": true
}"#)?;
// 加载配置
let config = Config::load("config.json")?;
println!("配置已加载:");
println!(" host: {}", config.host);
println!(" port: {}", config.port);
println!(" debug: {}", config.debug);
Ok(())
}示例 2:命令行工具错误处理
rust
▶ Runuse std::fs::File;
use std::io::{self, BufRead, BufReader};
use std::path::PathBuf;
use thiserror::Error;
#[derive(Error, Debug)]
enum CliError {
#[error("无法读取文件 '{0}': {1}")]
FileRead(PathBuf, io::Error),
#[error("无效的行 {line}: {reason}")]
InvalidLine { line: usize, reason: String },
#[error("参数错误:{0}")]
Argument(String),
}
struct LineProcessor {
file: PathBuf,
}
impl LineProcessor {
fn new(file: PathBuf) -> Self {
LineProcessor { file }
}
fn process(&self) -> Result<(), CliError> {
let file = File::open(&self.file)
.map_err(|e| CliError::FileRead(self.file.clone(), e))?;
let reader = BufReader::new(file);
for (line_num, line_result) in reader.lines().enumerate() {
let line = line_result
.map_err(|e| CliError::FileRead(self.file.clone(), e))?;
self.process_line(&line, line_num + 1)?;
}
Ok(())
}
fn process_line(&self, line: &str, line_num: usize) -> Result<(), CliError> {
// 模拟处理
if line.is_empty() {
return Err(CliError::InvalidLine {
line: line_num,
reason: "空行".to_string(),
});
}
println!("行 {}: {}", line_num, line);
Ok(())
}
}
fn main() {
let args: Vec<String> = std::env::args().collect();
if args.len() < 2 {
eprintln!("用法:{} <文件名>", args[0]);
std::process::exit(1);
}
let processor = LineProcessor::new(PathBuf::from(&args[1]));
if let Err(e) = processor.process() {
eprintln!("错误:{}", e);
std::process::exit(1);
}
}常见错误
错误 1:忽略 Result
rust
▶ Run// ❌ 错误:忽略错误
fn main() {
let _ = std::fs::remove_file("important.txt");
// 文件删除失败也没有处理
}
// ✅ 正确:处理错误
fn main() {
if let Err(e) = std::fs::remove_file("important.txt") {
eprintln!("删除文件失败:{}", e);
}
}错误 2:滥用 unwrap
rust
▶ Run// ❌ 错误:生产代码滥用 unwrap
fn read_config() {
let file = File::open("config.txt").unwrap();
// 如果文件不存在,程序会 panic
}
// ✅ 正确:使用适当的方法
fn read_config() -> Result<(), io::Error> {
let file = File::open("config.txt")
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
Ok(())
}错误 3:错误信息不明确
rust
▶ Run// ❌ 错误:模糊的错误信息
fn process() -> Result<(), String> {
File::open("data.txt")
.map_err(|_| "出错了".to_string())?; // 什么错误?
Ok(())
}
// ✅ 正确:保留原始错误信息
fn process() -> Result<(), io::Error> {
File::open("data.txt")?; // 保留完整错误
Ok(())
}错误 4:错误类型不统一
rust
▶ Run// ❌ 错误:混用多种错误类型
fn process() -> Result<(), Box<dyn Error>> {
let file = File::open("a.txt")?; // io::Error
let num: i32 = "abc".parse()?; // ParseIntError
Ok(())
}
// 调用者无法针对性处理
// ✅ 正确:定义统一的错误类型
#[derive(Error, Debug)]
enum AppError {
#[error("IO 错误:{0}")]
Io(#[from] io::Error),
#[error("解析错误:{0}")]
Parse(#[from] ParseIntError),
}
fn process() -> Result<(), AppError> {
let file = File::open("a.txt")?;
let num: i32 = "abc".parse()?;
Ok(())
}练习
练习 1:安全的除法
rust
▶ Runfn safe_divide(a: i32, b: i32) -> Result<i32, String> {
// 实现:处理除零错误
}练习 2:多字段验证
创建一个用户注册验证函数,验证:
- 用户名:3-20 个字符,只包含字母数字
- 邮箱:包含@和.
- 密码:至少 8 个字符,包含大小写字母和数字
返回所有验证错误。
练习 3:文件处理器
编写一个程序:
- 读取文件内容
- 每行解析为整数
- 计算总和
- 妥善处理所有可能的错误
小结
错误处理方法对比
| 方法 | 行为 | 适用场景 |
|---|---|---|
panic! | 终止程序 | 不可恢复错误 |
unwrap() | 成功返回值,失败 panic | 测试、原型 |
expect(msg) | 成功返回值,失败带消息 panic | 有明确预期时 |
match | 显式处理所有情况 | 需要精细控制 |
if let | 只处理成功/失败一种情况 | 另一种情况简单时 |
? | 传播错误 | 函数返回 Result |
核心要点
- 区分可恢复和不可恢复错误
- 使用 Result 处理预期错误
- 提供有意义的错误信息
- 不要忽略错误
- 使用自定义错误类型提高可维护性
小结
本章我们学习了:
- panic! 用于不可恢复错误,Result 用于可恢复错误
- 使用 ? 操作符传播错误,简化代码
- 自定义错误类型提供更好的类型安全和错误信息
- thiserror 库简化错误定义
- 错误处理最佳实践:提供上下文、尽早返回、不忽略错误
练习题
详见:练习题