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

Posted

技术标签:

【中文标题】这个实例如何看起来比它自己的参数生命周期更长?【英文标题】:How can this instance seemingly outlive its own parameter lifetime? 【发布时间】:2017-07-27 01:09:14 【问题描述】:

在我偶然发现下面的代码之前,我确信类型的生命周期参数中的生命周期总是比它自己的实例寿命长。换句话说,给定一个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);

尽管hint 创建的foo 似乎考虑的生命周期不会像它自己那么长,并且对它的引用被传递给更广泛范围内的函数,但代码的编译与它完全一样是。取消注释代码中所述的行会触发编译错误。或者,将Foo 更改为结构元组(PhantomData&lt;&amp;'a ()&gt;) 也会使代码不再编译时出现相同类型的错误(Playground)。

它是如何有效的 Rust 代码?这里编译器的推理是什么?

【问题讨论】:

哇,真奇怪!查看两个提示函数的 MIR,看起来 rust 在使用 PhantomData 时会放弃“生命周期”。 IDK,如果它是一个功能或一个错误:D 我怀疑答案与variance 有关,特别是在Tfn(T)逆变 的即兴评论——但是,我不太明白完成解释原因的任务。 【参考方案1】:

解释这一点的另一种方式是注意到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 的(更短的)生命周期。

【讨论】:

【参考方案2】:

尽管您的意图是最好的,但您的 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 更长寿),&amp;'a Tsubtype&amp;'b T

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

如果您对“子类型”与生命周期的应用感到困惑,那么让我用 Java 术语重新表述这个示例。让我们将&amp;'a () 替换为Programmer,将&amp;'b () 替换为Person。现在假设Programmer 派生自Person:因此ProgrammerPerson 的子类型。这意味着我们可以获取Programmer 类型的变量并将其作为参数传递给需要Person 类型参数的函数。这就是为什么以下代码将成功编译的原因:编译器会将T 解析为Person,用于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 是安全的,但你不能假设PersonProgrammer。同样,假设一个变量的生命周期较短是安全的,但你不能假设一个已知生命周期的变量实际上有一个更长的生命周期。毕竟,Rust 中生命周期的全部意义在于确保您不会访问超出其实际生命周期的对象。


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

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

通常,您会期望 Vec&lt;T&gt; 的所有元素都具有相同的类型(我们在这里不是在谈论 trait 对象)。但是方差让我们可以作弊。

&amp;'a T 是在'aT 之上的协变。这意味着无论我们在类型参数中看到&amp;'a T 的任何地方,我们都可以将其替换为&amp;'a T 的子类型。让我们看看结果如何:

fn main() 
    let a = ();
    let b = ();
    let v = vec![&a, &b];

我们已经确定ab 具有不同的生命周期,并且表达式&amp;a&amp;b 的类型不同1。那么为什么我们可以用这些来制作Vec?道理同上,所以总结一下:&amp;a被强制转换成&amp;'b (),这样v的类型就是Vec&lt;&amp;'b ()&gt;


fn(T) 是 Rust 中关于方差的一个特例。 fn(T)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();

这编译。但是quux 中的v 是什么类型?是Vec&lt;fn(&amp;'static ())&gt; 还是Vec&lt;fn(&amp;'a ())&gt;

我给你一个提示:

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

我们正在尝试使用 &amp;'a () 参数调用向量中的函数之一。但是v[0] 期望&amp;'static (),并且不能保证'a'static,所以这是无效的。因此我们可以得出结论v 的类型是Vec&lt;fn(&amp;'static ())&gt;。如您所见,逆变与协变相反:我们可以用更长的替换较短的生命周期。


哇,现在回到你的问题。首先,让我们看看编译器对hint 的调用产生了什么。 hint 具有以下签名:

fn hint<'a, Arg>(_: &'a Arg) -> Foo<'a>

Foo'a 相比逆变,因为Foo 包装了fn(或者更确切地说,假装,感谢PhantomData,但是当我们谈论方差时,这并没有什么区别;两者都具有相同的效果),fn(T)T 逆变,T 这里是&amp;'a ()

当编译器尝试解析对hint 的调用时,它只考虑shortlived 的生命周期。因此,hint 返回一个 Fooshortlived 的生命周期。但是当我们尝试将其分配给变量 foo 时,我们遇到了一个问题:类型的生命周期参数总是比类型本身的生命周期长,而 shortlived 的生命周期不会比 foo 的生命周期长,所以显然,我们不能将这种类型用于foo。如果Foo'a 是协变的,那就结束了,你会得到一个错误。但是Foo'a 相比逆变,因此我们可以将shortlived 的生命周期替换为更大 的生命周期。该生命周期可以是任何超过foo 生命周期的生命周期。请注意,“outlifes”与“strictly outlives”不同:区别在于 'a: 'a ('a outlives 'a) 为真,但 'a 严格超过 'a 为假(即生命周期是说比自己活得更久,但它并没有严格地比自己活得更久)。因此,我们最终可能会得到具有 Foo&lt;'a&gt; 类型的 foo,其中 'a 正是 foo 本身的生命周期。

现在让我们看看check(&amp;foo, &amp;outlived);(这是第二个)。这个编译是因为&amp;outlived 被强制,因此生命周期被缩短以匹配foo 的生命周期。这是有效的,因为outlived 的生命周期比foo 更长,并且check 的第二个参数与'a 是协变的,因为它是一个引用。

为什么check(&amp;foo, &amp;shortlived); 不编译? foo 的生命周期比 &amp;shortlived 长。 check 的第二个参数在 'a 上是协变的,但它的第一个参数是在 'a 上的逆变,因为 Foo&lt;'a&gt; 是逆变的。也就是说,这两个参数都试图将 'a 拉向相反的方向进行调用:&amp;foo 试图扩大 &amp;shortlived 的生命周期(这是非法的),而 &amp;shortlived 试图缩短 &amp;foo' s 生命周期(这也是非法的)。没有生命周期可以统一这两个变量,因此调用无效。


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

【讨论】:

这很可能是 Rust 标签中唯一的最佳答案。这里有很多信息,令人惊叹。感谢您花时间写它。 (我知道“谢谢”cmets 不赞成......但是来吧......看看这个答案)

以上是关于这个实例如何看起来比它自己的参数生命周期更长?的主要内容,如果未能解决你的问题,请参考以下文章

Spring bean的生命周期详解

如何理解 Vue 中的生命周期

如何理解 Vue 中的生命周期

bean的生命周期

Vue生命周期(详解版,会持续补充!!!)

Servlet生命周期与HTTP协议