使用 AsyncFn* 重构 Boluo Web 框架的异步闭包约束

这几天闲暇时,我对 boluo 框架的异步闭包约束完成了重构,核心优化点:让异步闭包能够返回借用其捕获值的 Future

本次重构基于 Rust 1.85.0 已稳定的 AsyncFn* 系列 Trait 实现,但想完成本次重构,还需要依赖返回类型标注(return_type_notation)。这一关键特性目前尚未稳定,它使我们可以标记 AsyncFn* 返回的 Future 具备 Send 特性。

因此,为使用该不稳定特性,我切换至 Rust Nightly 编译器完成开发,并将重构后的代码放到独立的分支 nightly-async-closures。等以后该特性稳定后,再将本次改动合并进主分支。

注:由于 AsyncFn* 的返回类型标注语法尚未最终确定,本次重构通过同样未稳定的 async_fn_traits 特性,实现了标记异步闭包返回的 FutureSend 的核心需求。

有什么不同?

有些人可能并不明白 AsyncFn* 解决了什么问题,下面我用几段简单的代码进行说明,希望能够帮助你理解此问题。

首先我们先构建新旧两种方案的异步闭包约束:

rust
// 旧方案:基于 Fn 约束
fn old_async_closures<F, Fut>(f: F)
where
    F: Fn() -> Fut,
    Fut: Future<Output = ()>,
{
}

// 新方案:基于 AsyncFn 约束
fn new_async_closures<F>(f: F)
where
    F: AsyncFn(),
{
}

这两种约束基本上是等价的,平时使用也几乎没有什么区别:

rust
// 都可以通过编译
old_async_closures(|| async {});
new_async_closures(|| async {});

old_async_closures(async || {});
new_async_closures(async || {});

这里需要指出:async || {} 是原生 AsyncFn 闭包;|| async {} 是返回 Future 的普通 Fn 闭包。

唯一的差异在于,返回的 Future 能否借用闭包的捕获值:

rust
// 编译失败!无法让 Future 借用闭包捕获的外部变量
let s = String::from("boluo");
old_async_closures(move || async {
    println!("{s}");
});
rust
// 编译通过!完美支持借用捕获的变量
let s = String::from("boluo");
new_async_closures(async move || {
    println!("{s}");
});

你可能会发现,这种写法看似可以正常运行:

rust
let s = String::from("boluo");
old_async_closures(|| async {
    println!("{s}");
});
new_async_closures(|| async {
    println!("{s}");
});

但这并不代表旧方案支持借用。

编译器很聪明,它发现闭包只需要捕获 s 的引用即可,于是 Future 只是复制了这份引用,并没有真正去借用闭包内部捕获的变量。

试想一下,如果 old_async_closures 真的拿走了 s 的所有权,那后面的 new_async_closures 根本就无法再使用变量 s 了。

项目地址