一个月前,Rust 官方发布了一篇博客 Changes to impl Trait
in Rust 2024,介绍了 Return Position Impl Trait (RPIT) 在使用和语法上的变化。新的设计对简化异步编程十分有用,故简述一下。
RPIT 举例(在线执行):
fn process(data: &Vec<u8>) -> impl Iterator<Item = u8> {
data.iter().map(|v| *v + 1)
}
上方的函数声明中隐藏了返回值的实际类型,返回 impl Iterator
表明它返回的是某种迭代器。调用方获得返回值后,也仅能通过迭代器的相关方法操作返回值。当然,编译器是知道该函数返回的是什么类型的,内部仍然是静态派发。
虽然调用者不知道返回的类型,但需要让它知道它会继续借用 data
参数,这样才能保证合法使用返回的迭代器。RPIT 原先的规则是必须显式地在函数声明中注明生命周期,所以上方的代码可以这样修改(在线执行):
fn process(data: &Vec<u8>) -> impl Iterator<Item = u8> + '_ {
data.iter().map(|v| *v + 1)
}
// 更加显式的声明
fn process<'d>(data: &'d Vec<u8>) -> impl Iterator<Item = u8> + 'd {
data.iter().map(|v| *v + 1)
}
对于原先的 RPIT 方案,官方列举了以下问题:
对于第 4 点,部分情况下当前 RPIT 的生命周期标注的表达能力是不足的,例如(在线执行):
fn process<'a, T: std::fmt::Display>(label: &'a str, data: Vec<T>) -> impl Iterator<Item = String> + 'a {
data.into_iter().map(move |v| format!("{}-{}", label, v))
}
上述代码会报错:the parameter type T
must be valid for the lifetime 'a
as defined here. 但实际上这一点是不需要的。出现该错误的原因有点微妙,因为返回的类型中我们需要使用到 label: 'a
,就目前的设计来看,我们只能把 'a
标注在返回类型的尾部。但显示地标注生命周期实际上产生了更严格的生命周期约束,+ 'a
的语义是返回的隐藏类型的生命周期是 'a
。但实际上该函数的返回类型是 std::iter::Map<impl Iterator<Item = T>, impl FnMut(T) -> String + 'a>
,当下的生命周期标注会间接地要求 T: 'a
,实际上这并不是需要的。当前的生命周期标注的表达能力无法处理该问题。
对于第 5 点,异步函数实际上是一种语法糖,它会将异步函数解糖为一个 RPIT 函数:
async fn process(data: &Vec<u8>) {
..
}
// desugared
fn process(
data: &Vec<u8>
) -> impl Future<Output = ()> {
async move {
..
}
}
但实际上,由于生命周期使用规则存在问题,这并不是真正的脱糖。真正的脱糖是一种允许使用所有生命周期的特殊 RPIT,但这种形式的 RPIT 并未向最终用户公开,与默认规则的 RPIT 并不一致。RPITIT 中也有类似的不一致问题。
对于上述问题,Rust 2024 的解决方案是这样的:
+ use<'x, T>
语法,精确指定使用到的生命周期新特性下,之前报错的代码可以顺利编译了(在线执行):
fn process(data: &Vec<u8>) -> impl Iterator<Item = u8> {
data.iter().map(|v| *v + 1)
}
原先 + 'a
表达能力不足的情况,也可以修改为 + use<'a, T>
来解决(在线执行):
fn process<'a, T: std::fmt::Display>(label: &'a str, data: Vec<T>) -> impl Iterator<Item = String> + use<'a, T> {
data.into_iter().map(move |v| format!("{}-{}", label, v))
}
// warning: all possible in-scope parameters are already captured, so `use<...>` syntax is redundant