Rust 如何为借用参数的 Future 表达生命周期约束?
可以用 for<'a> Fn(&'a Data) -> BoxFuture<'a, Vec<u8>> 表达这个约束,再通过普通包装函数将 async fn 返回的 Future 转成 BoxFuture。每次调用都可以使用各自的参数生命周期,回调也可以存入 Box<dyn Callback>。
你给出的 Callback::call<'a> 签名已经能表达这种关系。由于没有给出 BorrowingCallback 的具体实现和 SharedCallback::new 的定义,仅凭日志无法确定原代码中的哪一处约束导致了错误。
生命周期约束表达的是什么
下面的签名:
fn call<'a>(&self, data: &'a Data) -> BoxFuture<'a, Vec<u8>>;
表示调用者可以传入生命周期为 'a 的引用,返回的 Future 可以借用该引用,并受 'a 约束。不同调用可以选择不同的 'a。
for<'a> 要求回调能接受任意合法的参数借用生命周期,并不要求某一个已经创建的 Future 同时适用于所有生命周期。
常见的问题出现在类似这样的约束中:
F: for<'a> Fn(&'a Data) -> Fut,
Fut: Future<Output = Vec<u8>>,
这里的 Fut 是在 for<'a> 外部声明的单个类型参数,无法一般性地表示随输入生命周期变化的 Future 类型。借用参数的 async fn 返回的是不透明 Future 类型,其类型可能携带输入引用的生命周期关系。
使用 BoxFuture<'a, Vec<u8>> 后,返回类型中的 'a 就能直接与输入引用绑定。
用包装函数统一返回类型
示例沿用问题中简化代码的 Vec<u8> 输出类型,使用 futures_util::future::BoxFuture:
use futures_util::future::BoxFuture;
pub struct Data {
bytes: Vec<u8>,
}
pub trait Callback {
fn call<'a>(&self, data: &'a Data) -> BoxFuture<'a, Vec<u8>>;
}
impl<F> Callback for F
where
F: for<'a> Fn(&'a Data) -> BoxFuture<'a, Vec<u8>>,
{
fn call<'a>(&self, data: &'a Data) -> BoxFuture<'a, Vec<u8>> {
self(data)
}
}
async fn my_callback(data: &Data) -> Vec<u8> {
data.bytes.clone()
}
fn boxed_callback<'a>(data: &'a Data) -> BoxFuture<'a, Vec<u8>> {
Box::pin(my_callback(data))
}
async fn example() -> Vec<u8> {
let callback: Box<dyn Callback> = Box::new(boxed_callback);
let data = Data {
bytes: vec![1, 2, 3],
};
callback.call(&data).await
}
boxed_callback 的返回类型注解要求将具体的 Future 转换为受 'a 约束的 trait object:
fn boxed_callback<'a>(data: &'a Data) -> BoxFuture<'a, Vec<u8>>
如果只写 Box::pin(...),又没有合适的类型上下文,可能仍会得到 Pin<Box<具体 Future 类型>>,不能假定它会自动满足所有期望的高阶函数约束。
先使用命名包装函数,也能减少闭包生命周期推断带来的干扰。
原来的两层 trait 也可以保留
BorrowingCallback<'a> 与 Callback 的拆分本身可以成立。你可以保留原来的 Callback 及其 blanket implementation,再为已经返回 BoxFuture 的函数补上实现:
impl<'a, F> BorrowingCallback<'a> for F
where
F: Fn(&'a Data) -> BoxFuture<'a, Vec<u8>>,
{
fn call(&self, data: &'a Data) -> BoxFuture<'a, Vec<u8>> {
self(data)
}
}
随后传入前面的 boxed_callback。它对每个 'a 都具有匹配的输入与输出签名,因此能够满足 for<'a> BorrowingCallback<'a>。
这种写法与上一节直接为函数实现 Callback 的方案是两种替代方案,不需要同时加入。
为什么日志中的 'static 输出没有解决问题
原始日志使用的是:
Future<Output = &'static [u8]>
简化示例使用的是 Vec<u8>。两者需要各自保持输出类型一致,但处理生命周期问题的思路相同。
Output = &'static [u8] 只说明 Future 完成后产生的引用具有 'static 生命周期。Future 在执行期间仍可能保存 &'a Data,所以不能据此推断 Future 自身满足 'static。
日志中的 distinct uses of impl Trait result in different opaque types 说明编译器遇到了不匹配的不透明类型或相关泛型关系。它不足以证明编译器要求某个 Future “对所有生命周期都有效”;精确定位仍然需要缺失的实现代码。
dyn 兼容性与 FnOnce 要分别处理
当前 Callback 的方法只对生命周期进行泛型化,返回值又是明确的 BoxFuture,因此这种方法签名可以用于 dyn Callback。生命周期泛型不会像普通类型泛型方法那样阻止动态分发。
不过,最初的目标是 FnOnce,示例代码却通过 &self 调用回调。上面的实现选择了适合重复调用的 Fn。
如果确实需要消费回调,trait 的接收方式也应表达所有权转移。例如:
pub trait OnceCallback {
fn call<'a>(
self: Box<Self>,
data: &'a Data,
) -> BoxFuture<'a, Vec<u8>>;
}
impl<F> OnceCallback for F
where
F: for<'a> FnOnce(&'a Data) -> BoxFuture<'a, Vec<u8>>,
{
fn call<'a>(
self: Box<Self>,
data: &'a Data,
) -> BoxFuture<'a, Vec<u8>> {
let callback = *self;
callback(data)
}
}
调用 Box<dyn OnceCallback> 的 call 会消费这个盒子,从接口上保证回调只能使用一次。
还需要注意的两个约束
BoxFuture<'a, T> 包含 Send 约束。如果 Future 在暂停期间保存了不满足 Send 的值,需要调整这些值,或者在不要求跨线程移动 Future 的场景中使用 LocalBoxFuture<'a, T>。
另外,原签名只把返回值与 data 的借用关联。如果返回的 Future 还要借用回调对象自身,应将 self 的借用也关联起来:
fn call<'a>(&'a self, data: &'a Data) -> BoxFuture<'a, Vec<u8>>;
此时具体实现也要遵守该借用关系,不能在 Future 尚未结束时销毁被它借用的回调对象。
备注:内容仅供参考。
