如何为自定义结果枚举实现`?`操作符/`FromResidual` trait,以处理特定错误时的数据库保存逻辑
?操作符/FromResidual trait,以处理特定错误时的数据库保存逻辑 嘿,我完全懂你的痛点——既要用自定义结果类型区分普通错误和需要持久化的错误,又想让?操作符能像处理标准Result一样自动处理这些逻辑,还不想每次都写繁琐的match容易漏处理。这事儿是能搞定的,我一步步给你拆解:
第一步:让SavableResult支持?操作符
Rust的?操作符本质依赖于std::ops::Try trait,这个 trait 定义了类型如何转换为“成功值”或“剩余错误值”,以及如何从剩余值恢复。我们需要给你的SavableResult实现这个 trait:
首先补全必要的定义(比如你的ApiErr):
#[derive(Debug, Clone, PartialEq, Eq)] pub struct ApiErr { pub message: String, pub save_on_err: bool, // 其他业务字段... } #[derive(Copy, PartialEq, Eq, Debug, Clone)] pub enum SavableResult<T, E> { Ok(T), Err(E), SaveOnErr(E), }
然后定义一个“剩余值”类型,用来区分普通错误和需要保存的错误:
#[derive(Copy, PartialEq, Eq, Debug, Clone)] pub enum SavableResidual<E> { Err(E), SaveOnErr(E), }
接下来实现Try trait:
use std::ops::Try; impl<T, E> Try for SavableResult<T, E> { // 成功时返回的类型 type Output = T; // 失败时的剩余值类型 type Residual = SavableResidual<E>; // 将SavableResult转换为标准Result,供?处理 fn into_result(self) -> Result<Self::Output, Self::Residual> { match self { SavableResult::Ok(val) => Ok(val), SavableResult::Err(err) => Err(SavableResidual::Err(err)), SavableResult::SaveOnErr(err) => Err(SavableResidual::SaveOnErr(err)), } } // 从剩余值恢复为SavableResult,供?返回给上层函数 fn from_residual(residual: Self::Residual) -> Self { match residual { SavableResidual::Err(err) => SavableResult::Err(err), SavableResidual::SaveOnErr(err) => SavableResult::SaveOnErr(err), } } }
现在你的test2函数里的test3()?就能正常编译运行了!?会自动把SaveOnErr和Err都当作错误返回给上层,上层可以像你写的test1那样通过match处理保存逻辑。
第二步:解决业务场景中的自动保存痛点
你提到的登录失败时需要更新并保存auth实体的场景,直接在trait实现里硬编码保存逻辑不太现实(因为保存需要依赖数据库事务等上下文),但我们可以用通用包装函数来封装这个逻辑,避免重复写match:
首先调整SavableResult,让SaveOnErr携带需要保存的实体:
#[derive(Debug, Clone)] pub enum SavableResult<T, E, S> { Ok(T), Err(E), SaveOnErr(E, S), // S是要保存的实体类型 }
然后写一个通用的处理函数,自动处理保存逻辑:
async fn handle_savable<T, E, S>( result: SavableResult<T, E, S>, save_action: impl Fn(S) -> Result<(), E>, ) -> Result<T, E> { match result { SavableResult::Ok(val) => Ok(val), SavableResult::Err(err) => Err(err), SavableResult::SaveOnErr(err, entity) => { // 执行保存操作 save_action(entity)?; // 保存后返回错误 Err(err) } } }
现在你的execute函数就可以简化成这样:
pub async fn execute(&self) -> Result<JwtTokensPair, ApiErr> { let mut auth = AuthRepository::get_by_user_id(user_id, &trx).await?; // 假设login现在返回携带auth实体的SavableResult let login_result = auth.login(&self.password, self.hash_service); // 用包装函数自动处理保存 let tokens = handle_savable(login_result, |auth_to_save| { AuthRepository::save(&auth_to_save, &trx).await }).await?; trx.commit().await?; Ok(tokens) }
这样一来,你就不用每次手动写match判断是否需要保存,也不用担心漏处理这种特殊逻辑了。
额外优化:和标准Result互转
如果需要和标准库的Result类型互相转换,可以实现From trait:
impl<T, E> From<Result<T, E>> for SavableResult<T, E, ()> { fn from(result: Result<T, E>) -> Self { match result { Ok(val) => SavableResult::Ok(val), Err(err) => SavableResult::Err(err), } } }
这样数据库操作返回的Result可以直接用.into()转换成SavableResult,更方便。
备注:内容来源于stack exchange,提问作者ZiiMakc

