为啥作为参数传递的特征对象的生命周期需要更高等级的特征边界,而结构不需要?

Posted

技术标签:

【中文标题】为啥作为参数传递的特征对象的生命周期需要更高等级的特征边界,而结构不需要?【英文标题】:Why do the lifetimes on a trait object passed as an argument require Higher Ranked Trait Bounds but a struct doesn't?为什么作为参数传递的特征对象的生命周期需要更高等级的特征边界,而结构不需要? 【发布时间】:2018-11-29 11:41:25 【问题描述】:

当有一个 trait 对象传递给函数时如何处理生命周期?

struct Planet<T> 
    i: T,


trait Spinner<T> 
    fn spin(&self, value: T);


impl<T> Spinner<T> for Planet<T> 
    fn spin(&self, value: T) 


// foo2 fails: Due to lifetime of local variable being less than 'a
fn foo2<'a>(t: &'a Spinner<&'a i32>) 
    let x: i32 = 10;
    t.spin(&x);


// foo1 passes: But here also the lifetime of local variable is less than 'a?
fn foo1<'a>(t: &'a Planet<&'a i32>) 
    let x: i32 = 10;
    t.spin(&x);

(Playground)

此代码导致此错误:

error[E0597]: `x` does not live long enough
  --> src/main.rs:16:17
   |
16 |         t.spin(&x);
   |                 ^ borrowed value does not live long enough
17 |     
   |     - borrowed value only lives until here
   |
note: borrowed value must be valid for the lifetime 'a as defined on the function body at 14:5...
  --> src/main.rs:14:5
   |
14 |     fn foo2<'a>(t: &'a Spinner<&'a i32>) 
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

foo1 的函数签名与foo2 几乎相同。一个接收对 struct 的引用,另一个接收 trait 对象

我读到这是 Higher Ranked Trait Bounds 的用武之地。将 foo2 修改为 foo2(t: &amp;for&lt;'a&gt; Spinner&lt;&amp;'a i32&gt;) 会编译代码,但我不明白为什么。

为什么'a 不会缩小为x

引用the Nomicon:

我们到底应该如何表达F 的特征绑定上的生命周期?我们需要在那里提供一些生命周期,但是我们关心的生命周期在我们进入调用体之前无法命名!此外,这不是固定的生命周期。 call 适用于任何生命周期 &amp;self 在这一点上恰好有。

能否详细说明一下?

【问题讨论】:

【参考方案1】:

简而言之: foo1 可以编译,因为大多数类型都是泛型参数的变体,编译器仍然可以为 t 选择 Spinner impl。 foo2 无法编译,因为 Trait 在其泛型参数上是不变的,并且 Spinner impl 已经修复。


一些解释

我们来看第三版foo

fn foo3<'a>(t: &'a Planet<&'a i32>) 
    let x: i32 = 10;
    Spinner::<&'a i32>::spin(t, &x);

这会导致与您的foo2 相同的错误。里面发生了什么?

通过编写Spinner::&lt;&amp;'a i32&gt;::spin,我们强制编译器使用Spinner 特征的特定实现。而Spinner::&lt;&amp;'a i32&gt;::spin 的签名是fn spin(&amp;self, value: &amp;'a i32)。时期。生命周期'a 由调用者给出; foo 不能选。因此,我们必须传递一个至少存在'a 的引用。这就是发生编译器错误的原因。


那么为什么foo1会编译?提醒一下:

fn foo1<'a>(t: &'a Planet<&'a i32>) 
    let x: i32 = 10;
    t.spin(&x);

这里,'a 的生命周期也是由调用者给定的,foo1 不能选择。 但是foo1 可以选择使用Spinner 的哪个实现!请注意,impl&lt;T&gt; Spinner&lt;T&gt; for Planet&lt;T&gt; 基本上定义了无限多个特定实现(每个T 一个)。所以编译器也知道Planet&lt;&amp;'x i32&gt;确实实现了Spinner&lt;&amp;'x i32&gt;(其中'x是函数中x的具体生命周期)!

现在编译器只需要弄清楚它是否可以将Planet&lt;&amp;'a i32&gt; 转换为Planet&lt;&amp;'x i32&gt;。是的,它可以,因为most types are variant over their generic parameters 并且因此Planet&lt;&amp;'a i32&gt;Planet&lt;&amp;'x i32&gt; 的子类型,如果'a'x 的子类型(它是)。所以编译器只是将t“转换”为Planet&lt;&amp;'x i32&gt;,然后可以使用Spinner&lt;&amp;'x i32&gt; impl。


太棒了!但是现在到主要部分:为什么foo2 不编译呢?再次提醒一下:

fn foo2<'a>(t: &'a Spinner<&'a i32>) 
    let x: i32 = 10;
    t.spin(&x);

同样,'a 由调用者提供,foo2 无法选择它。不幸的是,现在我们已经有了一个具体的实现!即Spinner&lt;&amp;'a i32&gt;。我们不能仅仅假设我们被传递的东西也实现了Spinner&lt;&amp;'o i32&gt; 任何其他生命周期'o != 'a! Traits are invariant over their generic parameters.

换句话说:我们知道我们有一些东西可以处理至少与'a一样长的引用。但是我们不能假设我们得到的东西也可以处理比'a更短的生命周期!

举个例子:

struct Star;

impl Spinner<&'static i32> for Star 
    fn spin(&self, value: &'static i32) 


static SUN: Star = Star;

foo2(&SUN);

在此示例中,foo2 中的 'a'static。事实上,Star 实现 Spinner 仅用于 'statici32 的引用。


顺便说一句:这不是 trait 对象特有的!让我们看一下foo 的第四个版本:

fn foo4<'a, S: Spinner<&'a i32>>(t: &'a S) 
    let x: i32 = 10;
    t.spin(&x);

再次出现同样的错误。问题是,Spinner impl 已经修复了!和trait对象一样,我们只知道S实现了Spinner&lt;&amp;'a i32&gt;,不一定更多。

HRTB 来救援?

使用排名更高的特征边界可以解决问题:

fn foo2(t: &for<'a> Spinner<&'a i32>)

fn foo4<S: for<'a> Spinner<&'a i32>>(t: &S)

希望从上面的解释中可以清楚地看出,这是有效的,因为我们 Spinner 的特定 impl 不再固定!相反,我们再次有无限多的 impl 可供选择(每个 'a 一个)。因此我们可以选择'a == 'x所在的impl。

【讨论】:

谢谢!这很有见地。

以上是关于为啥作为参数传递的特征对象的生命周期需要更高等级的特征边界,而结构不需要?的主要内容,如果未能解决你的问题,请参考以下文章

为啥需要使用加号运算符 (Iterator<Item = &Foo> + 'a) 为特征添加生命周期?

lambda中对象的生命周期连接到pyqtSignal

为 dyn 对象实现特征时的神秘生命周期问题

为啥 Rust 编译器要求我限制泛型类型参数的生命周期(错误 E0309)?

为啥我们使用 - ngAfterContentInit 生命周期方法

c++中为啥要函数返回引用?