这个实例如何看似比自己的参数生命周期更长?

时间:2017-03-07 00:10:59

标签: rust lifetime

在我偶然发现下面的代码之前,我确信类型的生命周期参数中的生命周期总是比其自己的实例更长。换句话说,给定foo: Foo<'a>'a总是比foo更长。然后我被@Luc Danton(Playground)介绍了这个反辩论代码:

#[derive(Debug)]
struct Foo<'a>(std::marker::PhantomData<fn(&'a ())>);

fn hint<'a, Arg>(_: &'a Arg) -> Foo<'a> {
    Foo(std::marker::PhantomData)
}

fn check<'a>(_: &Foo<'a>, _: &'a ()) {}

fn main() {
    let outlived = ();
    let foo;

    {
        let shortlived = ();
        foo = hint(&shortlived);
        // error: `shortlived` does not live long enough
        //check(&foo, &shortlived);
    }

    check(&foo, &outlived);
}

即使由foo创建的hint似乎认为生命周期与其自身无关,并且对它的引用会传递给更广泛范围内的函数,代码完全按照原样编译。取消注释代码中声明的行会触发编译错误。或者,将Foo更改为struct tuple (PhantomData<&'a ()>)也会导致代码不再编译时出现相同类型的错误(Playground)。

Rust代码有效吗?这里编译器的原因是什么?

2 个答案:

答案 0 :(得分:43)

尽管您的意图很好,但您的hint功能可能没有您期望的效果。但是,在我们了解正在发生的事情之前,我们还有很多理由可以解决。

让我们从这开始:

fn ensure_equal<'z>(a: &'z (), b: &'z ()) {}

fn main() {
    let a = ();
    let b = ();
    ensure_equal(&a, &b);
}

好的,所以在main中,我们定义了两个变量ab。由于被不同的let语句引入,它们具有不同的生命周期。 ensure_equal需要两个相同生命周期的引用。然而,这段代码编译。为什么呢?

这是因为,'a: 'b(阅读:'a超过'b),&'a T&'b T a

我们假设'a的生命周期为b'b的生命周期为'a: 'b。这是a的事实,因为ensure_equal是先引入的。在调用&'a ()时,参数分别为&'b ()'a,分别为 1 。这里的类型不匹配,因为'b&'a ()的生命周期不同。但是编译器还没有放弃!它知道&'b ()&'a ()的子类型。换句话说,&'b () &a。因此,编译器将强制表达式&'b ()键入&'b (),以便两个参数都键入&'a ()。这解决了类型不匹配问题。

如果您对&#34; subtypes&#34;的应用感到困惑。有生命周期,那么让我用Java术语来重述这个例子。我们将Programmer替换为&'b (),将Person替换为Programmer。现在让我们说Person来自ProgrammerPerson因此是Programmer的子类型。这意味着我们可以获取类型为Person的变量,并将其作为参数传递给期望类型为T的参数的函数。这就是为什么以下代码将成功编译的原因:编译器将Person的{​​{1}}解析为main中的调用。

class Person {}
class Programmer extends Person {}

class Main {
    private static <T> void ensureSameType(T a, T b) {}

    public static void main(String[] args) {
        Programmer a = null;
        Person b = null;
        ensureSameType(a, b);
    }
}

这种子类型关系的非直观方面可能是寿命越长是寿命越短的子类型。但是可以这样想:在Java中,假装ProgrammerPerson是安全的,但你不能假设Person是一个Programmer Vec。同样,假装变量的更短生命周期是安全的,但您不能假设具有某些已知生命周期的变量实际上具有更长的 >一生。毕竟,Rust的整个生命周期都是为了确保您不会在实际生命周期内访问对象。

现在,让我们谈谈subtype。那是什么?

  

Variance是类型构造函数关于其参数的属性。 Rust中的类型构造函数是具有未绑定参数的泛型类型。例如,T是一个类型构造函数,它接受Vec<T>并返回&&mutVec<T>是带有两个输入的类型构造函数:生命周期和指向的类型。

通常情况下,您会期望&'a T的所有元素都具有相同的类型(我们此处并未讨论特征对象)。但差异让我们为此作弊。

'a 协变超过T&'a T。这意味着无论我们在类型参数中看到&'a T,我们都可以用fn main() { let a = (); let b = (); let v = vec![&a, &b]; } 的子类型替换它。让我们看看它是如何运作的:

a

我们已经确定b&a的生命周期不同,&bVec这两个词的类型不同 1 。那么为什么我们可以用这些来&a呢?推理与上述相同,因此我总结一下:&'b ()被强制转换为v,因此Vec<&'b ()>的类型为fn(T)

在变量方面,

fn(T)是Rust的一个特例。 T 逆变超过Vec。让我们构建一个fn foo(_: &'static ()) {} fn bar<'a>(_: &'a ()) {} fn quux<'a>() { let v = vec![ foo as fn(&'static ()), bar as fn(&'a ()), ]; } fn main() { quux(); } 函数!

v

这个编译。但quuxVec<fn(&'static ())>的类型是什么?是Vec<fn(&'a ())>还是fn foo(_: &'static ()) {} fn bar<'a>(_: &'a ()) {} fn quux<'a>(a: &'a ()) { let v = vec![ foo as fn(&'static ()), bar as fn(&'a ()), ]; v[0](a); } fn main() { quux(&()); }

我会给你一个提示:

error[E0495]: cannot infer an appropriate lifetime due to conflicting requirements
 --> <anon>:5:13
  |
5 |       let v = vec![
  |  _____________^ starting here...
6 | |         foo as fn(&'static ()),
7 | |         bar as fn(&'a ()),
8 | |     ];
  | |_____^ ...ending here
  |
note: first, the lifetime cannot outlive the lifetime 'a as defined on the body at 4:23...
 --> <anon>:4:24
  |
4 |   fn quux<'a>(a: &'a ()) {
  |  ________________________^ starting here...
5 | |     let v = vec![
6 | |         foo as fn(&'static ()),
7 | |         bar as fn(&'a ()),
8 | |     ];
9 | |     v[0](a);
10| | }
  | |_^ ...ending here
note: ...so that reference does not outlive borrowed content
 --> <anon>:9:10
  |
9 |     v[0](a);
  |          ^
  = note: but, the lifetime must be valid for the static lifetime...
note: ...so that types are compatible (expected fn(&()), found fn(&'static ()))
 --> <anon>:5:13
  |
5 |       let v = vec![
  |  _____________^ starting here...
6 | |         foo as fn(&'static ()),
7 | |         bar as fn(&'a ()),
8 | |     ];
  | |_____^ ...ending here
  = note: this error originates in a macro outside of the current crate

error: aborting due to previous error

这个没有编译。以下是编译器消息:

&'a ()

我们尝试使用v[0]参数调用向量中的一个函数。但是&'static ()需要'a,并且不能保证'staticv,所以这是无效的。因此,我们可以得出结论Vec<fn(&'static ())>的类型为hint。正如您所看到的,逆变量与协方差相反:我们可以用更长的替换短寿命。

哇,现在回到你的问题。首先,让我们看看编译器对hint的调用做了什么。 fn hint<'a, Arg>(_: &'a Arg) -> Foo<'a> 具有以下签名:

Foo

'a 逆变超过Foo因为fn包裹PhantomData(或者更确切地说,假装,感谢fn(T),但是当我们谈论方差时,这并没有什么不同;两者都有相同的效果),TT的逆变而&'a ()这里是hint

当编译器尝试解析对shortlived的调用时,它只会考虑hint的生命周期。因此,Foo会返回shortlived的{​​{1}}生命周期。但是当我们尝试将其分配给变量foo时,我们遇到了一个问题:类型上的生命周期参数总是比类型本身更长,而shortlived的生命周期并不存在超过foo的生命周期,显然,我们无法将此类型用于foo。如果Foo'a有协变性,那就是它的结束,你就会收到错误。但Foo 逆变超过'a,因此我们可以用更大的生命周期替换shortlived的生命周期。终生可以是任何超过foo一生的寿命。请注意&#34;超过&#34;与&#34;严格超过&#34;:不同之处在于'a: 'a'a超过'a)是真的,但'a严格超过{{ 1}}是假的(即一生被认为比自己寿命长,但它并没有严格地超过本身)。因此,我们最终可能会'a类型为foo,其中Foo<'a>恰好是'a本身的生命周期。

现在让我们看foo(第二个)。这个编译是因为check(&foo, &outlived);被强制执行,因此缩短了生命周期以匹配&outlived的生命周期。这是有效的,因为foo的有效期比outlived长,而foo的第二个参数比check更具协变性,因为它是&#39; sa参考

为什么没有'a编译? check(&foo, &shortlived);的有效期比foo长。 &shortlived的第二个参数是check的协变,但它的第一个参数是{em>逆变而不是'a,因为'a是逆变的。也就是说,这两个参数都试图在此调用的相反方向拉Foo<'a>'a正在尝试放大&foo的生命周期(这是非法的),而{&shortlived 1}}试图缩短&shortlived的生命周期(这也是非法的)。没有生命将统一这两个变量,因此调用无效。

1 这实际上可能是一种简化。我相信参考的生命周期参数实际上代表借用活动的区域,而不是参考的生命周期。在此示例中,对于包含对&foo的调用的语句,两个借位都将处于活动状态,因此它们将具有相同的类型。但是,如果您将借用拆分为单独的ensure_equal语句,则代码仍然有效,因此解释仍然有效。也就是说,为了使借用有效,所指对象必须比借用区域更长,所以当我想到终身参数时,我只关心指涉对象的生命,我认为借用是分开的

答案 1 :(得分:3)

解释这一点的另一种方法是注意Foo实际上并不包含对生命为'a的任何内容的引用。相反,它包含 接受 具有生命期'a的引用的功能。

您可以使用实际函数而不是PhantomData来构造此相同的行为。你甚至可以调用这个函数:

struct Foo<'a>(fn(&'a ()));

fn hint<'a, Arg>(_: &'a Arg) -> Foo<'a> {
    fn bar<'a, T: Debug>(value: &'a T) {
        println!("The value is {:?}", value);
    }
    Foo(bar)
}

fn main() {
    let outlived = ();
    let foo;
    {
        let shortlived = ();
        // &shortlived is borrowed by hint() but NOT stored in foo
        foo = hint(&shortlived);
    }
    foo.0(&outlived);
}

弗朗西斯在他的优秀答案中解释说,outlived的类型是shortlived类型的子类型,因为它的生命周期更长。因此,foo内的函数可以接受它,因为它可以被强制转换为shortlived(更短)的生命周期。