Rust 2024 Return Position Impl Trait 的变化

2024.10.11
SF-Zhou

一个月前,Rust 官方发布了一篇博客 Changes to impl Trait in Rust 2024,介绍了 Return Position Impl Trait (RPIT) 在使用和语法上的变化。新的设计对简化异步编程十分有用,故简述一下。

1. 背景

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)
}

2. 问题

对于原先的 RPIT 方案,官方列举了以下问题:

  1. 不是正确的默认行为:官方统计了 Rust 编译器和 crates.io 上的代码,大部分使用 RPIT 均需要使用生命周期,默认不使用生命周期的方案不方便
  2. 不够灵活:
  3. 难以解释:缺少生命周期声明时,难以向用户解释该错误
  4. 生命周期标注表达能力不足:将在下方举例说明
  5. 当前设计也与 Rust 的其他部分存在不一致,尤其是异步编程。

对于第 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 中也有类似的不一致问题。

3. 方案

对于上述问题,Rust 2024 的解决方案是这样的:

  1. 在默认情况下,RPIT 使用所有生命周期
  2. 新增 + 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

References

  1. Changes to impl Trait in Rust 2024
  2. Lifetime capture in the anonymous future