async fn 与 impl Future 的语法糖差异?

解读

在国内 Rust 社招/校招面试里,这道题几乎必问。面试官想确认三件事:

  1. 你是否把 async fn 只是返回一个匿名 Future 类型 这件事刻进 DNA;
  2. 你是否知道 impl Future 语法糖带来的“零开销”与“可命名”优势
  3. 你是否能在 跨 crate 边界、trait 定义、递归调用、Send/Sync 自动推导 等真实工程场景里,迅速判断该用哪一种写法。
    答成“一个方便、一个麻烦”只能拿 60 分;把 类型擦除、递归限制、Send 边界、trait object 安全 全部串起来,才能拿到 90+。

知识点

  1. async fn 的脱糖:编译器自动生成一个匿名状态机结构体(形如 async_fn_foo::{closure#0}),实现 Future<Output=T>,函数体所有局部变量变成状态机字段。
  2. impl Future 的脱糖:手写或 async { ... } 块直接返回一个具名或匿名类型,但返回类型可写进签名,因此可以:
    • 在 trait 里用 -> impl Future 而不破坏 object safety;
    • 在递归场景里把返回类型写成 BoxFuture<'_, T> 避免无限大小;
    • 在跨 crate 场景里给返回类型显式加 Send + 'static 边界,避免下游编译失败。
  3. Send/Sync 自动推导差异
    • async fn 只要内部持有 !Send 字段,整个返回类型就 !Send调用方无法强制修复
    • impl Future 写法可以把内部逻辑包进 async move {} 并手动 AssertSend隔离污染
  4. trait 里支持度
    • async fn 在 trait 中目前仍需 #[async_trait] 宏,代价是堆分配 + erased lifetime
    • -> impl Future 可直接写进 trait,零成本、无宏、原生异步调度器友好。
  5. 递归限制
    • async fn 不能自调用,因为返回的匿名类型在定义完成前大小未知;
    • impl Future 可返回 BoxFuture<'_, T> 或自定义 enum,显式打破递归循环
  6. 可见性与文档
    • async fn 返回的匿名类型在文档里只能看到 impl Future<Output=...>下游无法命名
    • impl Future 可以写成 pub struct MyFuture(...)下游可缓存、可特化、可写泛型约束

答案

async fn 只是编译器帮你写的 “返回匿名 Future 的状态机函数”;impl Future 则是 “把 Future 类型写进签名,让调用方可见、可命名、可约束” 的显式抽象。
核心差异可浓缩为四句:

  1. 类型是否具名:async fn 返回匿名类型,impl Future 可具名也可匿名,但签名里能看见。
  2. trait 支持:async fn 进 trait 需宏或 nightly,impl Future 稳定即写。
  3. Send/Sync 污染:async fn 一旦内部出现 !Send 就全局污染,impl Future 可通过 async move 局部隔离。
  4. 递归与大小:async fn 不能自调用,impl Future 可返回 BoxFuture 解决递归。
    因此,对外暴露的库 API、需要递归、需要 Send 边界、需要 trait object 安全时,优先用 impl Future;业务内部一键异步、无需命名、无需递归时,async fn 更简洁。

拓展思考

  1. 高并发网关场景,接口层用 -> impl Future + Send + 'static 把 IO 与 CPU 任务拆到不同线程,避免 async fn 隐式 !Send 导致整个链路阻塞
  2. 嵌入式 no_std 运行时,由于内存分配器缺失,不能用 BoxFuture,此时用 impl Future 手写状态机 enum,既零分配又满足递归。
  3. 面对下游 FFI 或 WASM 绑定,async fn 的匿名类型无法被 C/JS 侧命名,只能把 impl Future 包成 extern "C" 函数返回句柄,再在外部事件循环里轮询。
  4. 未来 trait async fn 稳定(AFIT) 后,官方允许在 trait 里直接写 async fn,但返回类型仍是匿名,库作者若想保持 API 稳定性,仍需 impl Future 或 type Alias impl Trait,否则一旦编译器升级导致状态机布局变化,就会破坏 ABI。