rust 随笔(二)
大约 8 分钟
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中。