如何返回 async fn in trait?
解读
在国内 Rust 岗位面试中,“trait 里怎么写 async fn” 几乎是中高级岗的必考题。
面试官真正想验证的是:
- 你是否知道 async fn 在底层会被编译成一个返回 impl Future 的泛型函数;
- 你是否明白 trait 里不能直接使用 async fn(直到 1.75 版本才稳定支持);
- 你是否能给出 稳定版可落地的三种主流方案(手动 impl Future、async-trait 宏、以及 1.75 之后的 AFIT),并权衡其性能与兼容性。
回答时务必结合 国内生产环境普遍停留在 1.70 左右 的现状,给出最稳妥的落地姿势,而不是只背语法。
知识点
- async fn 的 desugar:编译器会把
async fn foo() -> T
翻译成
fn foo() -> impl Future<Output = T> + '_.
其中隐藏了一个匿名关联类型,导致 trait 无法直接命名该类型。 - object safety:trait 里出现
async fn后,返回的impl Future会让 trait 丧失 object safety,无法生成 trait object(Box<dyn Trait>)。 - 稳定版解决方案
- 手动 impl Future:在 trait 里用关联类型把 Future 显式命名出来,手写 poll 或用 boxed() 擦除类型,兼容 1.56+,零宏依赖,但代码冗长。
- async-trait 宏:社区使用最广,自动把 async fn 展开成返回 Pin<Box<dyn Future + Send>>> 的普通函数,牺牲一次堆分配换来语法简洁,1.45+ 可用。
- AFIT(Async Fn In Trait):1.75 起稳定,允许直接在 trait 里写 async fn,编译器自动生成隐式关联类型,零堆分配,但目前仍需 nightly 特性
return_position_impl_trait_in_trait才能支持 trait object,生产环境需评估工具链版本。
- Send 约束:国内微服务 99% 跑在 Tokio 多线程调度器上,返回的 Future 必须 Send,否则编译期会报
future cannot be sent between threads。手写或 async-trait 时记得加Send边界。 - 生命周期:如果 trait 方法带
&self,返回的 Future 会捕获&self,必须显式声明生命周期,否则会出现cannot infer an appropriate lifetime错误。
答案
“在 当前国内生产环境普遍使用 1.70 及以下 的情况下,最稳妥的方案是 async-trait 宏。
步骤如下:
- 在 Cargo.toml 添加
async-trait = "0.1" - 在 trait 定义处加
#[async_trait],方法直接写 async fn:use async_trait::async_trait; #[async_trait] pub trait Cache { async fn get(&self, key: &str) -> Option<String>; } - 实现端同样加
#[async_trait],即可像普通 async fn 一样写逻辑:pub struct RedisCache; #[async_trait] impl Cache for RedisCache { async fn get(&self, key: &str) -> Option<String> { // 异步调用 redis todo!() } } - 如果团队已升级到 1.75+ 且无需 trait object,可直接用稳定版 AFIT:
但目前 trait object 仍需 nightly,因此线上服务若需要pub trait Cache { async fn get(&self, key: &str) -> Option<String>; }Box<dyn Cache>,仍建议回退到 async-trait 或手动 boxed。
一句话总结:稳定版优先 async-trait,1.75 以上可尝鲜 AFIT,务必验证 Send 与生命周期。”
拓展思考
- 性能敏感场景(如网关中间件)如何消除 async-trait 的堆分配?
可以 手写 impl Future + Send,用std::future::ready或poll_fn拼装状态机,完全零分配,但代码量翻倍,需权衡维护成本。 - trait object 的 Send 边界:
Box<dyn Cache>默认不 Send,需要把 trait 声明为
pub trait Cache: Send + Sync { ... }
否则上层 Tokio spawn 会编译失败。 - AFIT 与 RPITIT(Return Position Impl Trait In Trait):
一旦 RPITIT 稳定,trait 里既能写 async fn,又能返回 impl Future,同时支持 trait object,届时 async-trait 宏可能逐步退出历史舞台,但 升级节奏由国内发行版(OpenCloudOS、Anolis OS)的 Rust 包版本决定,面试时可主动提及“我们会跟随操作系统 LTS 节奏评估升级”,体现工程化思维。