基于Axum的Rust依赖注入与错误处理问题求助
问题描述
作为以C#开发为主、具备一定Rust经验的开发者,在基于Axum构建Web应用时,尝试抽象SQLx连接构建数据访问层,通过trait封装控制器对象并放入单一State实现依赖注入。此前将dyn trait包装在Arc中并要求实现Send + Sync,Axum可正常传递。
为避免原DataLayer trait遇到数据库错误直接panic,修改trait添加关联类型Error以支持自定义错误返回:
#[async_trait] pub trait DataLayer : Send + Sync { type Error : std::error::Error + Send + Sync; async fn try_get_user_by_id<'a>(&self, user_id: &'a str) -> Result<Option<UserDbModel>, Self::Error>; async fn get_user_by_email<'a>(&self, email: &'a str) -> Result<Option<UserDbModel>, Self::Error>; async fn get_refr_token_by_token<'a>(&self, token: &'a str) -> Result<Option<RefrTokenDbModel>, Self::Error>; async fn get_refr_token_by_id(&self, token: i32) -> Result<Option<RefrTokenDbModel>, Self::Error>; async fn create_refr_token(&self, refr_token: CreateRefrTokenDbModel) -> Result<u64, Self::Error>; async fn revoke_refr_token(&self, token: RevokeRefrTokenDbModel) -> Result<(), Self::Error>; }
具体实现(如DbDataLayer)使用sqlx::Error作为关联类型:
pub struct DbDataLayer { db: MySqlPool, settings: TokenSettings } #[async_trait] impl DataLayer for DbDataLayer { type Error = sqlx::Error; async fn try_get_user_by_id<'a>(&self, user_id: &'a str) -> sqlx::Result<Option<UserDbModel>> { let user = sqlx::query_as!(UserDbModel, r" SELECT id, email, password_hash as pwd_hash, role FROM users WHERE id = ? ", user_id).fetch_one(&self.db).await; match user { Ok(user) => Ok(Some(user)), Err(sqlx::Error::RowNotFound) => Ok(None) } } // 其他方法实现省略 }
但尝试注入时使用Arc<dyn DataLayer<Error = dyn Error + Send + Sync>>触发编译错误:
the size for values of type `(dyn StdError + Send + Sync + 'static)` cannot be known at compilation time the trait `Sized` is not implemented for `(dyn StdError + Send + Sync + 'static)`
需要解决:如何在支持Error类型泛化的同时修复编译错误?以及C#架构思路是否适配Rust?
编译错误解决方案
核心原因
Rust的trait关联类型默认要求实现Sized(编译时可知大小),而dyn Error + Send + Sync是未大小化类型(Unsized Type),无法满足关联类型的Sized约束,因此编译失败。
方案1:定义全局统一错误枚举(推荐)
创建一个包含所有可能错误类型的枚举,作为DataLayer的固定关联类型,既满足Sized要求,又能统一错误处理逻辑,符合Rust idiomatic风格。
步骤1:定义全局错误类型
#[derive(Debug)] pub enum AppError { Sqlx(sqlx::Error), // 可扩展添加其他错误类型,如业务逻辑错误、IO错误等 } // 实现Error trait impl std::error::Error for AppError {} // 实现Display trait,用于错误信息展示 impl std::fmt::Display for AppError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { AppError::Sqlx(e) => write!(f, "数据库错误: {}", e), // 其他错误类型的格式化逻辑 } } } // 实现From转换,方便将sqlx::Error自动转为AppError impl From<sqlx::Error> for AppError { fn from(e: sqlx::Error) -> Self { AppError::Sqlx(e) } }
步骤2:修改DataLayer trait
将关联类型固定为AppError:
#[async_trait] pub trait DataLayer : Send + Sync { type Error = AppError; async fn try_get_user_by_id<'a>(&self, user_id: &'a str) -> Result<Option<UserDbModel>, Self::Error>; // 其他方法签名保持不变 }
步骤3:修改DbDataLayer实现
利用?操作符自动将sqlx::Error转换为AppError:
#[async_trait] impl DataLayer for DbDataLayer { async fn try_get_user_by_id<'a>(&self, user_id: &'a str) -> Result<Option<UserDbModel>, AppError> { let user = sqlx::query_as!(UserDbModel, r" SELECT id, email, password_hash as pwd_hash, role FROM users WHERE id = ? ", user_id).fetch_one(&self.db).await?; Ok(Some(user)) } // 其他方法同理,使用?自动转换错误 }
注入使用
此时可以直接使用Arc<dyn DataLayer>,无需指定关联类型,编译正常:
let data_layer = Arc::new(DbDataLayer { db, settings }); let app = Router::new() .route("/user/:id", get(get_user_handler)) .with_state(data_layer);
方案2:使用装箱错误类型
如果需要保留错误类型的灵活性,可将关联类型指定为Box<dyn Error + Send + Sync + 'static>,通过装箱将未大小化类型转为大小化类型。
修改DataLayer trait
#[async_trait] pub trait DataLayer : Send + Sync { type Error = Box<dyn std::error::Error + Send + Sync + 'static>; async fn try_get_user_by_id<'a>(&self, user_id: &'a str) -> Result<Option<UserDbModel>, Self::Error>; // 其他方法签名保持不变 }
修改DbDataLayer实现
将sqlx::Error装箱后返回:
#[async_trait] impl DataLayer for DbDataLayer { async fn try_get_user_by_id<'a>(&self, user_id: &'a str) -> Result<Option<UserDbModel>, Box<dyn std::error::Error + Send + Sync + 'static>> { let user = sqlx::query_as!(UserDbModel, r" SELECT id, email, password_hash as pwd_hash, role FROM users WHERE id = ? ", user_id).fetch_one(&self.db).await; match user { Ok(user) => Ok(Some(user)), Err(sqlx::Error::RowNotFound) => Ok(None), Err(e) => Err(Box::new(e)), } } }
C#架构思路在Rust中的适配性分析
1. 依赖注入思路可行,但实现方式不同
Rust没有内置的DI容器(如ASP.NET Core的DI系统),Axum通过State结合Arc<dyn Trait>实现的手动DI是社区常用模式,你的思路完全适配Rust的Web开发场景。
2. 优先静态分发,必要时使用动态分发
C#依赖注入默认偏向动态分发(运行时解析),但Rust更鼓励静态分发(泛型):
- 如果不需要运行时切换数据层实现(如仅生产用SQLx,测试用Mock),可以将
CoreAuthService定义为泛型结构体:
这种方式编译时确定类型,避免trait对象的性能开销,更符合Rust的零成本抽象理念。pub struct CoreAuthService<T: DataLayer> { data_layer: T, } - 若需要运行时多态(如动态切换数据源),使用
Arc<dyn DataLayer>的动态分发是合理选择。
3. 错误处理需贴合Rust范式
C#使用异常处理错误,而Rust通过Result显式处理错误:
- 统一错误枚举(方案1)是Rust的惯用做法,它能让错误类型更明确,避免动态分发的开销,同时方便上层统一处理错误。
- 返回
Box<dyn Error>(方案2)虽然灵活,但会丢失错误类型的具体信息,上层处理时只能通过downcast获取具体类型,增加复杂度。
4. trait设计需注意Rust的约束
Rust的trait关联类型有Sized默认约束,这是你遇到编译错误的核心原因,设计trait时需要考虑类型大小的要求,避免使用未大小化类型作为关联类型。
内容的提问来源于stack exchange,提问作者Christian Schmid

