生命周期子类型化和 impl-trait

Lifetime sub-typing and impl-trait

我遇到了一种有趣的生命周期子类型化形式,我认为它是有效的,但编译器对此持怀疑态度。

考虑以下函数,它计算两个引用序列的点积。

fn dot_prod<'a>(xs: impl IntoIterator<Item = &'a usize>, ys: impl IntoIterator<Item = &'a usize>) -> usize {
    let mut acc = 0;
    for (x, y) in xs.into_iter().zip(ys.into_iter()) {
        acc += *x * *y;
    }
    acc
}

签名将相同的生命周期赋予两个序列中的引用。 “两个输入的单一生命周期”模式很常见,因为子类型允许函数用于不同生命周期的引用。然而,这里的某些东西(也许 impl trait?)阻止了它。让我们看一个被阻止使用的例子(playground link):

fn dot_prod_wrap<'a>(xs: impl IntoIterator<Item = &'a usize>, ys: impl IntoIterator<Item = &'a usize>) -> usize {
    let xs: Vec<usize> = xs.into_iter().cloned().collect();
    let ys = ys.into_iter();
    dot_prod(&xs, ys)
}

Rustc 拒绝了这一点,观察到局部 xs'a 无效,由此我们可以推断它已经为函数调用的生命周期参数插入了 'a。但是,我认为这应该进行类型检查,方法是插入本地作用域的生命周期(称之为 'b),并推断 ys 的类型是实现 [= 的东西的子类型18=],其中 'b 是本地范围。

它的一些变体确实有效,例如 changing the impl traits to slices and using two lifetime paramters,但我很好奇有一种方法可以让 Rust 编译器在不更改签名的情况下接受像 dot_prod_wrap 这样的 wapper dot_prod.

我也知道子类型和 impl trait 都很复杂,所以我上面提出的有效性论点可能是错误的。 “ys 的类型是实现 IntoIterator<Item = &'b usize> 的东西的子类型”这一说法尤其值得怀疑。

。一旦借用检查器推导出 impl IntoIterator<Item = &'a usize> 正确的生命周期 'a 就无法更改,即使是更短的生命周期。

原因是因为trait对象是黑盒子;他们可以在限制范围内做任何想做的事。这包括 iterior mutability 之类的东西,它不能使用更短的生命周期(否则你可以将生命周期更短的值分配给期望更长生命周期的对象)。它适用于切片,因为借用检查器知道生命周期是协变的;因此可以在需要时缩短它。

你应该dot_prod使用两个独立的生命周期。

but I'm curious about there is a way of getting the Rust compiler to accept a wapper like dot_prod_wrap without changing the signature of dot_prod.

关于 Rust 这样做的原因是正确的:只知道 ysIntoIterator::IntoIter 类型有一定的生命周期不足以让它知道它是否可以缩短寿命。 但是Iterator<Item=&'a usize>对我们程序员来说并不是一个不透明的黑盒子:

let ys = ys.into_iter().map(|y| y);

通过将迭代器中的所有值映射到它们自身,我们允许 Rust 为闭包选择合适的 return 类型。经过一番思考,rustc 确定为了进行 dot_prod 调用类型检查,需要缩短 returned 借用的生命周期以匹配 xs

.map(|y| y) 不会 类型系统之外的任何事情,因此 rustc 将在发布版本中完全优化它。 (它可以在所有情况下都这样做,因为这个闭包是 zero-sized 不透明类型。我不完全确定它 在所有情况下都如此 ,但我想不出来它不会这样做的原因。)