在使用 Rust 的过程中,相信很多朋友都有过类似的吐槽:真不确定自己要掌握多少语言知识、多少独门编程技巧和多么旺盛的好奇心,才能坚持做完这项最最琐碎的工作。绝望之下,我们往往会去 rust/issues 寻找解决办法,然后突然发现了一种在理论上根本不成立的 API 设计思路。这种矛盾源自某种微妙的语言Bug,简直神奇。
我从四年前开始接触Rust。目前为止,我写过相关的书和文章,也翻译了不少语言发布的公告。我还设法用Rust 编写过一些生产代码,甚至有幸在一场关注 Rust 的在线研讨上发过言。
虽然也算是身经百战,但我还是动不动就会跟Rust的借用检查器和类型系统“闹出”些不愉快。现在的我,虽然已经慢慢理解了Rust “无法返回对临时值的引用”之类的错误,也设计出一些启发式的策略来处理生命周期问题,但最近一个意外再次打击了我的信心……
初次尝试:用来处理更新的函数
我正打算编写一个聊天机器人,来改善用户的使用体验。通过长轮询或 webhooks,我开始一个个获取服务器更新流。我有一个面向全体更新的处理程序向量,其中每个处理程序都会接收对更新的引用,再把后续解析返回至()。这个处理程序向量由Dispatcher所有,每次有更新传入时,Dispatcher都会按顺序执行各个处理程序。
下面,试试具体实现。这里省略掉处理程序的执行部分,只关注push_handler函数。初次尝试:省略处理程序的执行,只关注push_handler函数。第一次尝试:
use futures::future::BoxFuture;use std::future::Future;
#[derive(Debug)]struct Update;
type Handler = Box<dyn for<'a> Fn(&'a Update) -> BoxFuture<'a, ()> + Send + Sync>;
struct Dispatcher(Vec<Handler>);
impl Dispatcher {
fn push_handler<'a, H, Fut>(&mut self, handler: H)
where
H: Fn(&'a Update) -> Fut + Send + Sync + 'a,
Fut: Future<Output = ()> + Send + 'a,
{
self.0.push(Box::new(move |upd| Box::pin(handler(upd))));
}
}
fn main() {
let mut dp = Dispatcher(vec![]);
dp.push_handler(|upd| async move {
println!("{:?}", upd);
});
}
在这里,我使用由HRTB生命周期for<'a>限制的动态类型Fn trait来表示每个更新处理程序。因为我希望返回的future由&'a Update函数参数中的'a部分决定。之后,我们又定义了拥有 Vec<Handler>的 Dispatcher类型。
在push_handler当中,我们接受一个静态类型的泛型H来返回Fut;为了将此类型的值推送至self.0,我们需要将处理程序打包至新的装箱处理程序当中,再使用Box::pin将返回的 future转换为来自futures箱的BoxFuture。
下面来看看这个解决思路行不行得通:
error[E0312]: lifetime of reference outlives lifetime of borrowed content...
--> src/main.rs:17:58
|17 | self.0.push(Box::new(move |upd| Box::pin(handler(upd))));
| ^^^
|
note: ...the reference is valid for the lifetime `'a` as defined here...
--> src/main.rs:12:21
|12 | fn push_handler<'a, H, Fut>(&mut self, handler: H)
| ^^
note: ...but the borrowed content is only valid for the anonymous lifetime #1 defined here
--> src/main.rs:17:30
|17 | self.0.push(Box::new(move |upd| Box::pin(handler(upd))));
很遗憾,这办法行不通。
问题在于push_handler会接收一个具体的生命周期'a ,也就是我们试图将HRTB的生命周期归结成for<'a>。在这种情况下,我们就需要证明for<'a, 'b> 'a: 'b(其中'b为来自push_handler 的'a),这显然不成立。
对于这个问题,我们可以尝试几种不同的处理方法:替换掉Fut泛型,转而强制要求user handler返回由 for<'a>限定的BoxFuture:
use futures::future::BoxFuture;
#[derive(Debug)]struct Update;
type Handler = Box<dyn for<'a> Fn(&'a Update) -> BoxFuture<'a, ()> + Send + Sync>;
struct Dispatcher(Vec<Handler>);
impl Dispatcher {
fn push_handler<H>(&mut self, handler: H)
where
H: for<'a> Fn(&'a Update) -> BoxFuture<'a, ()> + Send + Sync + 'static,
{
self.0.push(Box::new(move |upd| Box::pin(handler(upd))));
}
}
fn main() {
let mut dp = Dispatcher(vec![]);
dp.push_handler(|upd| {
Box::pin(async move {
println!("{:?}", upd);
})
});
}
现在编译部分没问题了,但最终得到的API还是有问题:理想情况下,我并不希望用户通过 Box::pin打包每个处理程序。毕竟push_handler才是专门干这个的,负责把静态类型的处理程序转换成动态类型空间中的等效形式。但如果我强行要求处理程序保持静态,又会如何?
要探究答案,我们可以用异构列表来试试。
第二次尝试:异构列表
异构列表这名称看着唬人,实际上就是大家熟悉的元组。也就是说,我们需要的是(H1, H2, H3, ...),其中每个H代表不同的处理程序类型。但同时,push_handler和execute 操作又要求我们