引用在嵌套结构中存活时间不足

7

我正在创建一系列包含对较低级别结构的可变引用的数据结构。我一直在愉快地使用下面的ABC,但我尝试添加一个新的层次D。实际上,ABCD是协议解码状态机的状态,但我已经在这里删除了所有内容。

struct A {}

fn init_A() -> A {
    A {}
}

struct B<'l> {
    ed: &'l mut A,
}

fn init_B(mut e: &mut A) -> B {
    B { ed: e }
}

struct C<'l> {
    pd: &'l mut B<'l>,
}

fn init_C<'l>(mut p: &'l mut B<'l>) -> C<'l> {
    C { pd: p }
}

struct D<'lifetime> {
    sd: &'lifetime mut C<'lifetime>,
}

fn init_D<'l>(mut p: &'l mut C<'l>) -> D<'l> {
    D { sd: p }
}

fn main() {
    let mut a = init_A();
    let mut b = init_B(&mut a);
    let mut c = init_C(&mut b);

    // COMMENT OUT THE BELOW LINE FOR SUCCESSFUL COMPILE
    let mut d = init_D(&mut c);
}

我遇到了一个错误:

error[E0597]: `c` does not live long enough
  --> src/main.rs:38:1
   |
37 |     let mut d = init_D(&mut c);
   |                             - borrow occurs here
38 | }
   | ^ `c` dropped here while still borrowed
   |
   = note: values in a scope are dropped in the opposite order they are created

我完全不理解在生命周期方面,D相对于C有什么不同:我不明白生命周期不匹配的含义。

这确实令人惊讶。就代码而言,您的代码在启用非词法生命周期的夜间版本上编译。 - Sven Marnach
我并不完全确定 NLL 版本的工作是否“正确”。它似乎存在至少一个错误 - Shepmaster
@Shepmaster 我也发现了这个 bug — 感谢你的报告!我不确定它是否与 NLL 有关。 - Sven Marnach
@SvenMarnach 是对的。正如注明的,非 NLL 版本报告了一个错误和一个警告,这让我认为 NLL 版本能够编译通过是不正确的事实。 - Shepmaster
如果有人需要帮助的话,这里是一个大幅简化的示例,展示了init_函数并不是必要的来统一生命周期。在结构体本身上也可以表现得一样。 - trent
1
@trentcl 不错!这是我回答中所提供的最小修复应用于该简化版本的链接:playground - Sven Marnach
2个回答

4
我将解释为什么问题中的代码无法工作。
简而言之,类型C<'l>和D<'l>在其生命周期内不变,并且它们使用单个生命周期参数('l)会导致这些类型的变量保持其借用,只要变量b存在,但是变量c(由d借用)在变量b之前被删除。
借用检查器本质上是一个约束求解器。它搜索满足各种约束条件的最短寿命0:引用不能比它所引用的值存活更长,生命周期必须遵守在函数签名和类型中指定的约束条件以及生命周期必须遵守方差规则1。
0- 引用的最短寿命是最好的,因为那样引用不会比必要时间更长地借用值。

1 — Rust有一个概念variance,它确定是否可以在期望寿命较短的值的地方使用具有更长寿命的值。Rustonomicon链接详细解释了这一点。

下面的代码是问题代码的简化版本,它失败了并显示相同的错误:c does not live long enough。块标记了变量的生存期。'a是变量a的生命周期,以此类推。这些生命周期由代码结构确定,并且它们是固定的。

类型注释中的生命周期(B(&'ar A) -> B<'ar>等)是变量。借用检查器试图找到固定生命周期('a'b'c、'd)对这些变量的有效分配。

let语句下面的注释显示了生命周期约束,我将在下面解释。

struct A;

struct B<'l>(&'l mut A);

struct C<'l>(&'l mut B<'l>);

struct D<'l>(&'l mut C<'l>);

fn main() {
    // lifetime 'a
    let mut a = A;
    { // lifetime 'b
        // B(&'r mut A) -> B<'ar>   
        let mut b = B(&mut a); 
        // 'r >= 'ar & 'r <= 'a
        { // lifetime 'c
            // C(&'br mut B<'ar>) -> C<'abr>  
            let mut c = C(&mut b); 
            // 'br <= 'b & 'abr = 'ar & 'br >= 'abr
            { // lifetime 'd
                // D(&'cr mut C<'abr>) -> D<'cabr> 
                let d = D(&mut c); 
                // 'cr <= 'c & 'cabr = 'abr & 'cr >= 'cabr
            }
        }
    }
}

第一项任务

// B(&'r mut A) -> B<'ar>   
let mut b = B(&mut a); 
// 'r <= 'a & 'r >= 'ar

引用a不能超出a的生存期,因此'r <= 'a

&'r mut A在'r上有不同的变体,因此我们可以将其传递到期望&'ar mut AB<'ar>类型构造函数中,当且仅当'r >= 'ar

第二个任务

 // C(&'br mut B<'ar>) -> C<'abr>  
 let mut c = C(&mut b); 
 // 'br <= 'b & 'abr = 'ar & 'br >= 'abr

参考值不能超过b ('br <= 'b),&mut B对于B是不变的 ('abr = 'ar),&'br mut B对于'br是可变的 ('br >= 'abr)

d的分配类似于c

Rust似乎不会考虑它尚未遇到的生命周期作为可能的分配。因此,'ar的可能分配为'a'b,而'abr的可能分配为'a'b'c等。

这组约束可以简化为'ar = 'abr = 'cabr,而'ar的最小允许赋值是'b。因此,bcd的类型分别为B<'b>C<'b>D<'b>。也就是说,变量d在生命周期'b中持有对c的引用,但c会在生命周期'c结束时被丢弃。

如果我们移除d,那么c仍然会将b借用到生命周期'b的结束,但这并不是问题,因为b不会超出生命周期'b

这个描述仍然是简化的。例如,虽然c的类型为C<'b>,但c并未借用整个生命周期'bb,而是在c定义后的一部分'b中借用它,但我还不太理解。

2
您原始代码中的init_*()函数总是返回一个类型,其生命周期参数等于您传递的引用的生命周期。由于您以这种方式构建了一条引用链,因此所有生命周期最终都将相同,abcd的类型最终将分别为AB<'a>C<'a>D<'a>。在c之前这没问题,因为生命周期'a可以是b的作用域,这满足所有约束条件。
然而,一旦加入d,就没有单独的生命周期'a可以使所有引用都有效。生命周期'a不能再是b的作用域,因为c的寿命不够长。它也不能是c的作用域,因为这对b来说太短了,所以编译器会报错。
通过解耦生命周期,每个变量都可以拥有自己的生命周期,从而使一切都按预期工作。由于问题仅从D开始,因此在那一点引入一个额外的生命周期就足够了。
struct A;

fn init_a() -> A {
    A {}
}

struct B<'a> {
    ed: &'a mut A,
}

fn init_b(ed: &mut A) -> B {
    B { ed }
}

struct C<'b> {
    pd: &'b mut B<'b>,
}

fn init_c<'b>(pd: &'b mut B<'b>) -> C<'b> {
    C { pd }
}

struct D<'c, 'b: 'c> {
    sd: &'c mut C<'b>,
}

fn init_d<'c, 'b: 'c>(sd: &'c mut C<'b>) -> D<'c, 'b> {
    D { sd }
}

fn main() {
    let mut a = init_a();
    let mut b = init_b(&mut a);
    let mut c = init_c(&mut b);
    let d = init_d(&mut c);
}

Playground 链接


1
让原始代码编译的另一种方法:playground let (mut b, mut c, d); 使bcd的生命周期完全相同。 - red75prime
@red75prime,你应该继续并将其作为答案(以及为什么它有效的解释)。 - Shepmaster
我知道我可以调整代码使其编译:例如,在我的实际用例中,我可以完全停止使用引用,并将a的所有权转移给b,然后将b的所有权转移给c等等 - 这样一切都会在d处同时超出范围。我真正想知道的是为什么这个代码在加入D时无法编译,而在ABC时却可以。 - Ben Clifford
1
@Shepmaster,我有一个答案的草稿,但是我需要让它更清晰,对每个人来说,包括我自己。这涉及到复杂的生命周期相互作用。 - red75prime
@red75prime 确实。也许nikomatsakis和pnkfelix之间的一些讨论会有所帮助? - Shepmaster
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接