Skip to content

错误处理实战总结

> 综合运用 Result、自定义错误类型和最佳实践,构建健壮的错误处理系统。

错误处理最佳实践

1. 使用 Result 处理预期错误

rust
// ✅ 推荐:使用 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
}
▶ Run

2. 提供有意义的错误信息

rust
// ✅ 推荐:使用 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")?;
▶ Run

3. 不要忽略错误

rust
// ❌ 错误:忽略错误
let _ = File::open("important.txt");

// ✅ 正确:至少记录错误
if let Err(e) = File::open("important.txt") {
    eprintln!("警告:无法打开文件:{}", e);
}
▶ Run

4. 尽早返回

rust
// ❌ 不推荐:深层嵌套
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(())
}
▶ Run

5. 收集多个错误

rust
// 验证多个字段时,收集所有错误
#[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)
    }
}
▶ Run

完整示例

示例 1:配置文件解析器

rust
use 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(())
}
▶ Run

示例 2:命令行工具错误处理

rust
use 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);
    }
}
▶ Run

常见错误

错误 1:忽略 Result

rust
// ❌ 错误:忽略错误
fn main() {
    let _ = std::fs::remove_file("important.txt");
    // 文件删除失败也没有处理
}

// ✅ 正确:处理错误
fn main() {
    if let Err(e) = std::fs::remove_file("important.txt") {
        eprintln!("删除文件失败:{}", e);
    }
}
▶ Run

错误 2:滥用 unwrap

rust
// ❌ 错误:生产代码滥用 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(())
}
▶ Run

错误 3:错误信息不明确

rust
// ❌ 错误:模糊的错误信息
fn process() -> Result<(), String> {
    File::open("data.txt")
        .map_err(|_| "出错了".to_string())?;  // 什么错误?
    Ok(())
}

// ✅ 正确:保留原始错误信息
fn process() -> Result<(), io::Error> {
    File::open("data.txt")?;  // 保留完整错误
    Ok(())
}
▶ Run

错误 4:错误类型不统一

rust
// ❌ 错误:混用多种错误类型
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(())
}
▶ Run

练习

练习 1:安全的除法

rust
fn safe_divide(a: i32, b: i32) -> Result<i32, String> {
    // 实现:处理除零错误
}
▶ Run

练习 2:多字段验证

创建一个用户注册验证函数,验证:

  • 用户名:3-20 个字符,只包含字母数字
  • 邮箱:包含@和.
  • 密码:至少 8 个字符,包含大小写字母和数字

返回所有验证错误。

练习 3:文件处理器

编写一个程序:

  1. 读取文件内容
  2. 每行解析为整数
  3. 计算总和
  4. 妥善处理所有可能的错误

小结

错误处理方法对比

方法行为适用场景
panic!终止程序不可恢复错误
unwrap()成功返回值,失败 panic测试、原型
expect(msg)成功返回值,失败带消息 panic有明确预期时
match显式处理所有情况需要精细控制
if let只处理成功/失败一种情况另一种情况简单时
?传播错误函数返回 Result

核心要点

  1. 区分可恢复和不可恢复错误
  2. 使用 Result 处理预期错误
  3. 提供有意义的错误信息
  4. 不要忽略错误
  5. 使用自定义错误类型提高可维护性

小结

本章我们学习了:

  • panic! 用于不可恢复错误,Result 用于可恢复错误
  • 使用 ? 操作符传播错误,简化代码
  • 自定义错误类型提供更好的类型安全和错误信息
  • thiserror 库简化错误定义
  • 错误处理最佳实践:提供上下文、尽早返回、不忽略错误

练习题

详见:练习题