跳至主要內容

rust 随笔(二)

Mr.Lexon大约 8 分钟rust

Rust 随笔(二)

这篇文章探讨在Rust中自定义Error如何设计,我们先直接给出一个例子:

use std::fmt;

#[derive(Debug)]
enum CustomError {
    CustomErrorKind1,
    CustomErrorKind2(String),
    CustomErrorKind3(String, String),
}
//实现Display,用于呈现错误结构
impl fmt::Display for CustomError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            CustomError::CustomErrorKind1 => write!(f, "Custom Error Type 1 Occurred"),
            CustomError::CustomErrorKind2(s) => write!(f, "Custom Error Type 2 Occurred: {}", s),
            CustomError::CustomErrorKind3(s1, s2) => write!(f, "Custom Error Type 3 Occurred: {} - {}", s1, s2),
        }
    }
}

//通用化
impl std::error::Error for CustomError {}

// Example of how to use it:
fn main() {
    let error1 = CustomError::CustomErrorKind1;
    let error2 = CustomError::CustomErrorKind2("Something went wrong".to_string());
    let error3 = CustomError::CustomErrorKind3("File not found".to_string(), "Ensure the path is correct".to_string());

    println!("{}", error1);
    println!("{}", error2);
    println!("{}", error3);

    // For debug formatting
    println!("{:?}", error1);
    println!("{:?}", error2);
    println!("{:?}", error3);
}

这是一个非常简单和常见的错误,事实上会出现一个问题,如果这个错误是一个中转错误(比如说打开文件失败的IO错误),那么原错误就看不到了,如下例子:

use std::fmt;
use std::fs;
use std::io;
use std::error::Error; // 引入 Error trait

// --- 自定义错误类型 ---
#[derive(Debug)]
enum AppError {
    // 只存储一个描述,不存储原始 IO 错误
    FileAccessError(String),
    ComputationError(String),
}

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            AppError::FileAccessError(msg) => write!(f, "文件访问错误: {}", msg),
            AppError::ComputationError(msg) => write!(f, "计算错误: {}", msg),
        }
    }
}

// 最简单的 Error trait 实现,没有 source()
impl Error for AppError {
    // fn source(&self) -> Option<&(dyn Error + 'static)> { None } // 默认就是 None
}

// 模拟读取文件,可能会产生 io::Error
fn read_data_from_file(path: &str) -> Result<String, io::Error> {
    fs::read_to_string(path) // 这个函数会返回 Result<String, io::Error>
}

// 高层函数,调用 read_data_from_file 并将错误转换为 AppError
fn process_data(file_path: &str) -> Result<String, AppError> {
    let data = read_data_from_file(file_path).map_err(|io_err| {
        // 问题在这里:我们创建了一个新的错误,但只传递了一个通用的消息,
        // 丢失了 io_err 的具体类型和详细信息(比如具体是 NotFound 还是 PermissionDenied 等)
        AppError::FileAccessError(format!("无法从 '{}' 读取数据", file_path))
        // 如果我们想在这里打印 io_err,是可以的,但它没有被传递到 AppError 内部
        // eprintln!("底层IO错误: {:?}", io_err); // 这只是打印,没有包装
    })?;

    // 假设这里还有一些计算,也可能出错
    if data.contains("invalid") {
        return Err(AppError::ComputationError("数据包含无效内容".to_string()));
    }

    Ok(data.to_uppercase())
}

// --- 主函数 ---
fn main() {
    let file_path = "non_existent_file.txt"; // 一个不存在的文件

    println!("尝试处理文件: {}", file_path);
    match process_data(file_path) {
        Ok(content) => {
            println!("处理后的数据: {}", content);
        }
        Err(app_err) => {
            eprintln!("\n错误报告 (没有 source)");
            eprintln!("应用程序错误: {}", app_err); // 只会显示 FileAccessError 的 Display 信息

            // 尝试追踪 source,但因为没有实现,所以会是 None
            if let Some(source) = app_err.source() {
                eprintln!("  根本原因: {}", source);
            } else {
                eprintln!("  (没有可用的根本原因信息,原始IO错误细节丢失)");
            }
        }
    }
}

结果就是:

尝试处理文件: non_existent_file.txt 

错误报告 (没有 source)
应用程序错误: 文件访问错误: 无法从 'non_existent_file.txt' 
读取数据 (没有可用的根本原因信息,原始IO错误细节丢失)

那么如何改进呢,只需要在这上面加一个source(),如下图:

use std::fmt;
use std::fs;
use std::io;
use std::error::Error;

//自定义错误类型 (改进版)
#[derive(Debug)]
enum AppError {
    // 现在 FileAccessError 包含原始的 io::Error
    FileAccessError {
        msg: String,
        source: io::Error, // 直接存储具体的 io::Error 类型
    },
    ComputationError(String),
}

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            AppError::FileAccessError { msg, .. } => write!(f, "文件访问错误: {}", msg), // Display 可以保持简洁
            AppError::ComputationError(msg) => write!(f, "计算错误: {}", msg),
        }
    }
}

impl Error for AppError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            AppError::FileAccessError { source, .. } => Some(source), // 返回对内部 io::Error 的引用
            AppError::ComputationError(_) => None,
        }
    }
}

//业务逻辑函数 (错误转换部分修改)

fn read_data_from_file(path: &str) -> Result<String, io::Error> {
    fs::read_to_string(path)
}

fn process_data_v2(file_path: &str) -> Result<String, AppError> {
    let data = read_data_from_file(file_path).map_err(|io_err| {
        // 现在我们将原始的 io_err 存储起来
        AppError::FileAccessError {
            msg: format!("无法从 '{}' 读取数据", file_path),
            source: io_err, // 关键:保存原始错误
        }
    })?;

    if data.contains("invalid") {
        return Err(AppError::ComputationError("数据包含无效内容".to_string()));
    }

    Ok(data.to_uppercase())
}

// --- 主函数 (调用改进版) ---
fn main() {
    let file_path = "non_existent_file.txt";

    println!("尝试处理文件: {}", file_path);
    match process_data_v2(file_path) {
        Ok(content) => {
            println!("处理后的数据: {}", content);
        }
        Err(app_err) => {
            eprintln!("\n 错误报告 (带有 source)");
            eprintln!("应用程序错误: {}", app_err); // AppError 的 Display

            let mut current_error: Option<&(dyn Error + 'static)> = Some(&app_err);
            let mut cause_level = 0;
            while let Some(source_err) = current_error.and_then(|err| err.source()) {
                cause_level += 1;
                eprintln!("  根本原因 (层级 {}): {}", cause_level, source_err); // 打印原始错误
                // 如果原始错误本身也实现了 source(),可以继续追踪,但 io::Error 的 source 通常是 None
                current_error = Some(source_err);
                if source_err.source().is_none() { // 避免无限循环(如果source()返回自身)
                    break;
                }
            }
            if cause_level == 0 {
                 eprintln!("  (此错误没有更深层的根本原因)");
            }
        }
    }
}

结果:

尝试处理文件: non_existent_file.txt 

错误报告 (带有 source)
应用程序错误: 文件访问错误: 无法从 'non_existent_file.txt' 读取数据 
根本原因 (层级 1): No such file or directory (os error 2)

这个设计基本涵盖了很多场景,不过要是遇到更加复杂的场景,比如说不同的错误变体,错误嵌套,那么我们就得引入error sturct的概念了,举个例子:

use std::fmt;
use std::fs;
use std::io;
use std::error::Error;
use std::path::{Path, PathBuf}; 

#[derive(Debug)]
pub struct ConfigError {
    path: PathBuf,                // 出错的配置文件路径
    kind: ConfigErrorKind,       // 具体的错误种类
}

#[derive(Debug)]
pub enum ConfigErrorKind {
    Io(io::Error),
    Format(serde_json::Error), // 假设使用 serde_json 解析配置
    MissingField(String),      // 缺少关键配置项
}

impl fmt::Display for ConfigError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "配置文件错误 '{}': {}", self.path.display(), self.kind)
    }
}

impl Error for ConfigError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match &self.kind {
            ConfigErrorKind::Io(ref err) => Some(err),
            ConfigErrorKind::Format(ref err) => Some(err),
            ConfigErrorKind::MissingField(_) => None,
        }
    }
}

// 为了方便,也为 ConfigErrorKind 实现 Display
impl fmt::Display for ConfigErrorKind {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ConfigErrorKind::Io(err) => write!(f, "IO错误 ({})", err),
            ConfigErrorKind::Format(err) => write!(f, "格式错误 ({})", err),
            ConfigErrorKind::MissingField(field) => write!(f, "缺少配置项 '{}'", field),
        }
    }
}


// 2. 数据库错误
#[derive(Debug)]
pub struct DatabaseError {
    db_name: String,            // 目标数据库名
    operation: String,          // 执行的操作,如 "connect", "authenticate"
    kind: DatabaseErrorKind,
}

#[derive(Debug)]
pub enum DatabaseErrorKind {
    Connection(io::Error),
    Authentication(String),   // 认证失败原因
    PoolTimeout,              // 连接池超时
}

impl fmt::Display for DatabaseError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "数据库 '{}' 操作 '{}' 失败: {}", self.db_name, self.operation, self.kind)
    }
}

impl Error for DatabaseError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match &self.kind {
            DatabaseErrorKind::Connection(ref err) => Some(err),
            DatabaseErrorKind::Authentication(_) => None,
            DatabaseErrorKind::PoolTimeout => None,
        }
    }
}

impl fmt::Display for DatabaseErrorKind {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            DatabaseErrorKind::Connection(err) => write!(f, "连接错误 ({})", err),
            DatabaseErrorKind::Authentication(reason) => write!(f, "认证失败 ({})", reason),
            DatabaseErrorKind::PoolTimeout => write!(f, "连接池超时"),
        }
    }
}


// 3. 查询错误 (可以类似地定义,这里简化)
#[derive(Debug)]
pub struct QueryError {
    query: String,
    reason: String,
    source: Option<Box<dyn Error + Send + Sync + 'static>>, // 通用源错误
}

impl fmt::Display for QueryError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "查询 '{}' 失败: {}", self.query, self.reason)
    }
}

impl Error for QueryError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        self.source.as_ref().map(|b| &**b as &(dyn Error + 'static))
    }
}


// 顶层错误 Enum
// 这个 Enum 聚合了所有特定类型的错误结构体
#[derive(Debug)]
pub enum AppError {
    Config(ConfigError),
    Database(DatabaseError),
    Query(QueryError),
    Io(io::Error), // 有时也可能直接暴露一些通用的 IO 错误
    Initialization(String), // 其他初始化错误
}

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            AppError::Config(err) => write!(f, "{}", err), // 直接调用 ConfigError 的 Display
            AppError::Database(err) => write!(f, "{}", err), // 直接调用 DatabaseError 的 Display
            AppError::Query(err) => write!(f, "{}", err),
            AppError::Io(err) => write!(f, "IO 错误: {}", err),
            AppError::Initialization(msg) => write!(f, "初始化错误: {}", msg),
        }
    }
}

impl Error for AppError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            AppError::Config(err) => Some(err),    
            AppError::Database(err) => Some(err),  
            AppError::Query(err) => Some(err),     
            AppError::Io(err) => Some(err),        
            AppError::Initialization(_) => None,
        }
    }
}

// 为了方便从具体的错误 struct 转换为 AppError,我们可以实现 From trait
impl From<ConfigError> for AppError {
    fn from(err: ConfigError) -> Self {
        AppError::Config(err)
    }
}

impl From<DatabaseError> for AppError {
    fn from(err: DatabaseError) -> Self {
        AppError::Database(err)
    }
}

impl From<QueryError> for AppError {
    fn from(err: QueryError) -> Self {
        AppError::Query(err)
    }
}

impl From<io::Error> for AppError { // 有时也希望直接转换IO错误
    fn from(err: io::Error) -> Self {
        AppError::Io(err)
    }
}


//使用示例
fn load_config(path: &Path) -> Result<(), AppError> {
    let content = fs::read_to_string(path).map_err(|io_err| AppError::Config(ConfigError{
        path: path.to_path_buf(),
        kind: ConfigErrorKind::Io(io_err),
    }))?;

    // 模拟 JSON 解析错误
    // serde_json::from_str(&content).map_err(|json_err| AppError::Config(ConfigError{
    //     path: path.to_path_buf(),
    //     kind: ConfigErrorKind::Format(json_err),
    // }))?;

    // 模拟缺少字段
    if !content.contains("required_field") {
        return Err(AppError::Config(ConfigError{
            path: path.to_path_buf(),
            kind: ConfigErrorKind::MissingField("required_field".to_string()),
        }));
    }
    Ok(())
}

fn connect_to_db(db_name: &str) -> Result<(), AppError> {
    if db_name == "prod_readonly" {
        Err(DatabaseError {
            db_name: db_name.to_string(),
            operation: "connect".to_string(),
            kind: DatabaseErrorKind::Authentication("read-only user cannot connect directly".to_string()),
        }.into()) // 使用 From trait 转换
    } else {
        // 模拟IO连接错误
        // Err(AppError::Database(DatabaseError {
        //     db_name: db_name.to_string(),
        //     operation: "connect".to_string(),
        //     kind: DatabaseErrorKind::Connection(io::Error::new(io::ErrorKind::ConnectionRefused, "server down")),
        // }))
        Ok(())
    }
}


fn main() {
    let config_path = PathBuf::from("my_app.cfg");
    match load_config(&config_path) {
        Ok(_) => println!("配置加载成功!"),
        Err(e) => {
            eprintln!("主错误: {}", e);
            let mut current_source = e.source();
            while let Some(source) = current_source {
                eprintln!("  源自: {}", source);
                current_source = source.source();
            }
            eprintln!("---");
        }
    }

    match connect_to_db("prod_readonly") {
        Ok(_) => println!("数据库连接成功!"),
        Err(e) => {
            eprintln!("主错误: {}", e);
            let mut current_source = e.source();
            while let Some(source) = current_source {
                eprintln!("  源自: {}", source);
                current_source = source.source();
            }
            eprintln!("---");
        }
    }
}

在这个例子中,每一种错误都可以直接转换使用,每一种错误都显性的表明了对应的业务类型,并且在函数使用过程中统一了签名,这种方式极大的改善了代码的可读性与耦合性。但是这种方式在一般程序中相当的复杂,所以,如果在做一个体量较小的程序时,可以直接采用第一种方法,针对系统开发,则强烈建议第二种方法,首要进行分层设计,然后再根据分层设计结构汇总到同一个ErrorEnum中。