这几天闲暇时,我对 boluo 框架的异步闭包约束完成了重构,核心优化点:让异步闭包能够返回借用其捕获值的 Future。
本次重构基于 Rust 1.85.0 已稳定的 AsyncFn* 系列 Trait 实现,但想完成本次重构,还需要依赖返回类型标注(return_type_notation)。这一关键特性目前尚未稳定,它使我们可以标记 AsyncFn* 返回的 Future 具备 Send 特性。
因此,为使用该不稳定特性,我切换至 Rust Nightly 编译器完成开发,并将重构后的代码放到独立的分支 nightly-async-closures。等以后该特性稳定后,再将本次改动合并进主分支。
注:由于
AsyncFn*的返回类型标注语法尚未最终确定,本次重构通过同样未稳定的async_fn_traits特性,实现了标记异步闭包返回的Future为Send的核心需求。
有什么不同?
有些人可能并不明白 AsyncFn* 解决了什么问题,下面我用几段简单的代码进行说明,希望能够帮助你理解此问题。
首先我们先构建新旧两种方案的异步闭包约束:
// 旧方案:基于 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(),
{
}
这两种约束基本上是等价的,平时使用也几乎没有什么区别:
// 都可以通过编译
old_async_closures(|| async {});
new_async_closures(|| async {});
old_async_closures(async || {});
new_async_closures(async || {});
这里需要指出:
async || {}是原生AsyncFn闭包;|| async {}是返回Future的普通Fn闭包。
唯一的差异在于,返回的 Future 能否借用闭包的捕获值:
// 编译失败!无法让 Future 借用闭包捕获的外部变量
let s = String::from("boluo");
old_async_closures(move || async {
println!("{s}");
});
// 编译通过!完美支持借用捕获的变量
let s = String::from("boluo");
new_async_closures(async move || {
println!("{s}");
});
你可能会发现,这种写法看似可以正常运行:
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 了。