为什么作为参数传递的特征对象的生存期要求较高的特质边界,而结构则不需要?

时间:2018-06-20 10:53:43

标签: rust lifetime

将特征对象传递给函数时,如何处理生存期?

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对象的引用。

我读到了这是进入更高特质界限的地方。将{em> foo2 修改为foo2(t: &for<'a> Spinner<&'a i32>)可以编译代码,但是我不明白为什么。

为什么'a不会缩水x

引用the Nomicon

  

我们到底该如何表达F的特质界限上的生命?我们需要在此处提供一些生命周期,但是直到我们进入调用主体之前,我们才能指定我们关心的生命周期!而且,这不是固定的寿命; call可以在任何生命周期内&self使用。

请详细说明吗?

1 个答案:

答案 0 :(得分:8)

简而言之:foo1会进行编译,因为大多数类型在其通用参数上有所不同,并且编译器仍可以为Spinner选择一个t隐含形式。 foo2无法编译,因为特征在其通用参数上是不变的,并且Spinner impl已经固定。


一些解释

我们来看看foo的第三版:

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

这将导致与您的foo2相同的错误。那里发生了什么事?

通过编写Spinner::<&'a i32>::spin,我们强制编译器使用Spinner特性的特定实现。 Spinner::<&'a i32>::spin的签名为fn spin(&self, value: &'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<T> Spinner<T> for Planet<T>基本上定义了许多特定的实现(每个T一个)。因此,编译器还知道Planet<&'x i32>确实实现了Spinner<&'x i32>(其中'x是函数中x的特定生存期)!

现在,编译器只需确定是否可以将Planet<&'a i32>转换为Planet<&'x i32>。是的,它可以,因为如果Planet<&'a i32>Planet<&'x i32>的子类型,most types are variant over their generic parameters'a就是'x的子类型。因此,编译器仅将t转换为Planet<&'x i32>,然后可以使用Spinner<&'x i32> impl。


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

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

同样,'a由呼叫者提供,foo2无法选择它。不幸的是,现在我们已经有一个特定的实现!即Spinner<&'a i32>。我们不能仅仅假设我们被传递的东西在其他任何生命周期Spinner<&'o i32>内也实现了'o != 'aTraits are invariant over their generic parameters

换句话说:我们知道我们有 something 可以处理寿命至少为'a的引用。但是我们不能认为我们所处理的东西还可以处理比'a更短的生命!

例如:

struct Star;

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

static SUN: Star = Star;

foo2(&SUN);

在此示例中,'a中的foo2'static。实际上,Star仅对Spinner'static的引用实现i32


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

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

再次出现相同的错误。问题再次出在Spinner impl已经修复!与特征对象一样,我们只知道S实现了Spinner<&'a i32>,而不一定实现更多。

HRTB获救了吗?

使用排名较高的特征范围可以解决此问题:

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

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

从上面的解释中可以很明显地看出来,这是可行的,因为我们不再固定Spinner的具体含义!取而代之的是,我们又有无限多个提示可供选择(每个'a一个)。因此,我们可以在'a == 'x处选择隐含符号。