【问题标题】:Why can a generic lifetime parameter in Rust be specialized to two disjoint lifetimes for one object?为什么 Rust 中的通用生命周期参数可以专门用于一个对象的两个不相交的生命周期?
【发布时间】:2021-08-10 09:29:41
【问题描述】:

在下面的代码中,我试图了解通用生命周期参数'a 是如何专门化的。

struct Wrapper<'a>(&'a i32);

fn foo() {
    let mut r;

    {
        let x = 0;       // the lifetime of x, call it 'x, starts from here |
        r = Wrapper(&x); // 'a parameter in Wrapper is specialized to 'x    |
        drop(r);         //                                                 |
    }                    //  --------------------------------- 'x ends here |         

    {
        let y = 0;       // the lifetime of y, call it 'y, starts from here   |
                         // 'y is distinct from 'x                            |
                         // neither outlives the other one                    |
        r = Wrapper(&y); // why 'a parameter can again be specialized to 'y?  |
        drop(r);         //                                                   |
    }                    // ------------------------------------ 'y ends here |
}

为什么一个通用生命周期参数 'a 可以专门化为两个不相交的生命周期 'x'y,用于一个对象 'r

换个角度看,我对r的具体类型很困惑。从 Rustonomicon 的 subtyping and variance 章节中,我了解到生命周期 'a 是泛型类型 Wrapper&lt;'a&gt; 的一部分。当泛型被特化时,它不能是Wrapper&lt;'x&gt; 也不能是Wrapper&lt;'y&gt;。那么r的类型是什么?

也许higher-rank trait bound与它有关?

如果有人能解释一下 Rust 编译器是如何在内部解释这一点的,我将不胜感激。

更新:

现有答案表明r 的生命周期可以有多个起点和终点。如果这是真的,那么'r'x'y 的联合。但是,这种解释并不适合subtyping 规则。例如,考虑下面的代码,'r2'r 都不是另一个的子类型(或寿命更长),所以我们应该不能调用bar(r, r2),但实际上可以。矛盾。

struct Wrapper<'a>(&'a i32);

fn bar<'a>(_p: Wrapper<'a>, _q: Wrapper<'a>) {}

fn foo() {
    let mut r;

    {
        let z = 0;
        let r2 = Wrapper(&z);               // -> |
        {                                   //    |
            let x = 0;       // -> |        //    +--'r2
            r = Wrapper(&x); //    |--'x--+ //    |
            bar(r, r2);      //    |      | // <- |
        }                    // <- |      |
    }                        //           |
    {                        //           +--'r
        let y = 0;           // -> |      |
        r = Wrapper(&y);     //    |--'y--+
        drop(r);             //    |
    }                        // <- |
}

【问题讨论】:

  • Rustonomicon 有一个标题为The area covered by a lifetime 的部分,其中指出:“一生可以有一个停顿。或者您可能会将其视为两个不同的借用,只是被绑定到同一个局部变量。

标签: rust lifetime


【解决方案1】:

我希望我的想法不会与编译器内部实际发生的事情有太大分歧。

rustc_middle (MIR) 的直接答案:

  1. let mut r; 的类型推断为Wrapper&lt;'free_region&gt;,其中'free_region 没有附加边界。
  2. 大致上,每个表达式都有自己的区域实例,其边界取决于它们所在的范围。在每个地方,MIR 都会对特定区域边界是否满足进行局部推理。对于这种情况:r = Wrapper(&amp;x); 它检查r's 类型中的'free_region 是否可以被'x 替换,并且因为前者没有边界(因为[1])它成功。请注意,它会在本地进行替换,因此其他作用域看不到它,它们会看到它们的本地替换。

让我们将'free_region 绑定到外部范围:

struct Wrapper<'a>(&'a i32);

fn foo<'a>() {
    let mut r: Wrapper<'a>;

    {
        let x = 0;
        r = Wrapper(&x); //~ ERROR `x` does not live long enough
        drop(r);
    }
}

人们可能会想但是r 被丢弃在'x 的范围内!原因再次,本地推理


更新:值得注意的是,r 只是一个绑定,而不是可能来自 OOP 理解的对象。就是这样,一个类型的名称,您将右值分配给两次。 (右值表示表达式值)

【讨论】:

    【解决方案2】:

    您以错误的方式思考生命周期。 r 的生命周期不会专门化为 'x'y。它有自己的生命周期(我们称之为'r)。

    正如Non-Lexical Lifetimes RFC 中所解释的,值的生命周期(或 RFC 所称的范围)是程序上将来仍可能使用该值的点。至关重要的是,生命周期不一定是线性的,也不必适应特定的语法范围(即 {} 块)。此外,生命周期不连接到变量(如r),而是绑定到它们的值。因此,如果您分配给一个变量(即r = ..),您实际上会杀死一个值并启动另一个值。它不算作使用。但是,分配给一个值的成员(即r.0 = ..),一种用法,因为您正在更改现有值的一小部分。

    在您的情况下,'r 有两个起点和两个终点。第一个起点在r = Wrapper(&amp;x);,第一个端点在第一个drop(r)。第二个起点和终点位于 y 块中。可视化:

    struct Wrapper<'a>(&'a i32);
    fn foo() {
        let mut r;
    
        {
            let x = 0;       
            r = Wrapper(&x); // -> |
                             //    |--'x--+
            drop(r);         // <- |      |      
        }                    //           |
        {                    //           +--'r
            let y = 0;       //           |
            r = Wrapper(&y); // -> |      |
                             //    |--'y--+
            drop(r);         // <- |
        }
    }
    

    这里我还插入了'x'y以供参考。注意'r 是如何在两个作用域之间存在的。

    当借用检查时,生命周期的要求是生命周期需要彼此。假设您有绑定'a: 'b'a 必须比'b 寿命更长)。为了保持这一点,'b 在任何地方都处于活动状态,'a 也必须处于活动状态。这就是寿命的意思。还有另一种说法:'a lasts longer than 'b。 然而,在这里我们还必须考虑到借用检查是位置感知的。这意味着'a only needs to outlive 'b starting from the point their relation begins.

    让我们看看我们的例子。对于像r = Wrapper(&amp;'x x) 这样的语句(我添加了生命周期以供参考),借用检查器必须确保引用&amp;'x x 的生命周期超过r(即'x: 'r)从r = Wrapper(&amp;x) 开始的生命周期。看看上面的生命周期,我们可以看到'x 的任何地方都是活动的,'r 也是活动的。 'x 会在作用域结束时消失,但幸运的是,'r 也会消失。所以这检查了。冲洗并重复'y


    为了对此更有信心,让我们看一下示例的一些变体,看看我们如何预测编译器会接受它还是拒绝它。

    示例 1

    我们将简单地删除 drop 调用,看看它是否有所作为:

    struct Wrapper<'a>(&'a i32);
    fn foo() {
        let mut r;
    
        {
            let x = 0;
            r = Wrapper(&x);
        }        
    
        {
            let y = 0;
            r = Wrapper(&y);
        }
    }
    

    这不会改变任何生命周期。请记住,生命周期关乎未来是否会使用这些值。由于我们没有添加任何 rxy 的用法,因此没有生命周期变化。 正如预期的那样,it compiles just fine

    示例 2

    如果x 不在范围内怎么办:

    struct Wrapper<'a>(&'a i32);
    fn foo() {
        let mut r;
    
        let x = 0;
        r = Wrapper(&x);
        
        {
            let y = 0;
            r = Wrapper(&y);
        }
    }
    

    这里'r'x 已移入父范围,但除此之外它们都与原始示例非常相似。 'y 也是如此。因此,所有界限仍然成立,it compilesy 也是一样。

    示例 3

    所以让我们更改r 的一些用法。如果我们不为y 创建一个新的Wrapper,而是简单地覆盖引用:

    struct Wrapper<'a>(&'a i32);
    fn foo() {
        let mut r;
        {
            let x = 0;
            r = Wrapper(&x);
        }
        {
            let y = 0;
            r.0 = &y;
        }
    }
    

    这在很大程度上改变了'r,因为在第一个作用域之后,它将在第二个作用域中使用,以分配新的引用。因此,'r 现在从r = Wrapper(&amp;x) 跨越到r.0 = &amp;y。然而,'x 不能匹配这个,因为x 不会活得足够长(它超出了范围)。因此,我们应该得到一个错误,说x 寿命不够长,which we do

    示例 4

    如果我们只在两个作用域后使用r 会怎样:

    #[derive(Debug)]
    struct Wrapper<'a>(&'a i32);
    fn foo() {
        let mut r;
        {
            let x = 0;
            r = Wrapper(&x);
        }
        {
            let y = 0;
            r = Wrapper(&y);
        }
        println!("{:?}", r)
    }
    

    在这里,'r 现在比原始示例稍大,在r = Wrapper(&amp;y) 之后继续到达打印。但是,它仍然不在两个块之间,因为在第一个作用域中分配的值从未在第二个作用域中使用。因此,'x'r 寿命长。然而,'y 现在需要足够大才能到达打印件(因为'r 这样做),但它不能这样做,因为y 将超出范围。因此,编译器应该抱怨y 的寿命不够长,而对x 只字不提。幸运的是,that is the case


    关于子类型

    问题的更新询问为什么上述内容似乎与子类型规则相矛盾。给定一个函数fn some_fn&lt;'a&gt;(a: &amp;'a i32, b: &amp;'a i32),为什么我们可以向这个函数传递两个不同生命周期的引用。在这两种情况下,函数声明似乎需要完全相同的生命周期。

    答案是子类型有specific rules for lifetimes

    每当引用从一个位置复制到另一个位置时,Rust 子类型化规则要求源引用的生命周期超过目标位置的生命周期。

    这意味着rust永远不会检查两个生命周期是否相等。它只检查它们是否以正确的方式彼此长寿。在更新的示例中,'a 是对 bar 的调用的生命周期。当将rr2 传递给它时,我们得到了'r: 'a'r2: 'a 的边界。这很容易,因为'a 仅在通话期间有效,其他生命周期也有效。调用之后,所有三个生命周期都死了,因此绑定微不足道。

    【讨论】:

    • 感谢您的调查。我刚刚在问题中添加了另一个示例。但是,新示例似乎与您的解释相矛盾。
    • “对于像 r = Wrapper(&'xx) […] 这样的语句,类型检查器必须确保引用 &'xx 的生命周期比 r 的生命周期长。” 实际上恰恰相反:r 借用了x 所以x 必须比r 活得更久。
    • @Jmb 你是对的,我把它弄反了。我编辑了答案以更正此问题并扩展以解释为什么它仍然有效。
    • @Zhiyao 我添加了一些关于子类型的内容。另请注意,'r'r2 不需要比对方活得更久,但每个人都比 'a 活得更久,他们都这样做。
    猜你喜欢
    • 2013-07-03
    • 1970-01-01
    • 2019-03-04
    • 1970-01-01
    • 1970-01-01
    • 2019-01-06
    • 2014-09-10
    • 2015-12-08
    相关资源
    最近更新 更多