"dropping out of scope" 和 "being moved" 在 Rust 中的重借行为有何不同?

how do "dropping out of scope" and "being moved" behave differently on reborrow in Rust?

如果我理解正确的话,在 Rust 中,一个重借的生命周期必须比它重借的生命周期短:

let mut x = ...;
let m1 = &mut x;
let m2 = &mut *m;
let m3 = m1; // m1 moved here
let m4 = m2; // use m2 here

m1移动到m3会触发错误,因为m1m2借用了m2,后面会用到

但是,在这种情况下,退出范围的行为有所不同,如下所示:

let mut x = ...;
let m2 : &mut ...;
{
    let m1 = &mut x;
    m2 = &mut *m1;
} // m1 drop out of scope here
let m4 = m2; // use m2 here

如果 m2 是对 m1 的借用,而不是重新借用,则两种情况都无法编译。

现在,我明白为什么编译器的行为是安全的了。在 moving 的情况下,一个人最终有两个可变借用到同一个变量,这是不合理的。在退出范围的情况下,可变借用无论如何都会死亡,并且重新借用仍然指向有效数据。 但我想知道:

这可以用生命周期来“解读”,其实远不止于此。生命周期不仅仅是描述编译器完成的过于复杂的操作的便捷方式:它们 编译器用来处理所有权和借用的抽象。也就是说,编译器将计算 每个 变量的生命周期,因此它知道何时必须为这些变量释放内存。

让我们逐步了解它如何在您的示例中执行此操作。请注意,'lifetime: { ... } 不是有效语法,但我们将使用 来表示 'lifetime 此作用域的生命周期 。同样,let a: 'a = ... 不是有效语法,但我们用它来表示 a 的生命周期是 'a.

目标如下:如果我有一个绑定到范围的生命周期,这是一个 已知的 生命周期,因为我知道它确保该值的时间有多长一辈子都会住。因此,我应该能够创建作用域,使变量的生命周期与作用域的生命周期完全一致,并且尊重变量生命周期之间的关系。

第一个例子

我们从命名每一世开始。

struct Foo; // Does not implement `Copy`

'outer: {           // Code has to live in a scope (it might be `main`, for example)
  let mut x: 'x = Foo;
  let m1: 'm1 = &mut x;
  let m2: 'm2 = &mut *m1;
  let m3: 'm3 = m1; // m1 moved here
  let m4: 'm4 = m2; // use m2 here
}

我们可能看到的第一个关系是:

  • 'x: 'm1;
  • 'x: 'm2;
  • 'x: 'm3;
  • 'x: 'm4.

These are to be read 'x outlives 'mX,意思是最后,'x有效的范围必须包含'mX的范围。

其他关系是:

    由于 m3; 的定义,
  • 'm3 必须在 'm1 结束的地方开始
  • 类似地,'m4 必须从 'm2 结束的地方开始,因为 m2.
  • 的定义

这些是由于&mut TCopy,所以m1m3必须移动

其他约束是由于 Rust 的独占借用规则,'mX 必须与 'mY 不相交,因为 XY

此外,隐含地,'outer 比一切都长寿。在这个例子中,你可能会认为 'outer'static,因为唯一的要求是 'static 必须比一切都长。

现在我们已经枚举了约束条件,我们可以尝试解决问题,即“解决未知数'x'm1'm2'm3'm4”。第一步是将生命周期分配给 'x,因为这很简单。除了 'outer 之外,它没有被任何人超过的限制,这是一个作用域的生命周期,所以让我们分配 'x = 'outer。由于我们隐含的假设,这符合前四个约束,实际上使它们无效(根据 'outer 的定义它们是正确的,所以不用再担心了)。

然后,我们要尝试找到满足移动条件的范围。

'outer: {
  let mut x: 'x = Foo;
  let m1: 'm1 = &mut x;               // ---------\
                                      //          |- `'m1`
  let m2: 'm2 = &mut *m1;             // ---------+--\
                                      //          |  |
  let m3: 'm3 = m1;                   // ---------/  |- `'m2`
                                      //             |
  let m4: 'm4 = m2;                   // ------------/
}

我们在这里没有太多的自由,因为我们被告知 'm1'm2 开始和结束的确切时间。现在矛盾来了:'m1'm2 一定是不相交的,他们显然不是!也就是说,您将无法找到满足所有条件的范围,就像我们为 'x 所做的那样。

第二个例子

让我们像在第一个示例中那样命名。

'outer: {
  let mut x: 'x = Foo;
  let m2: 'm2;
  'inner: {
    let m1: 'm1 = &mut x;
    m2 = &mut *m1;
  }
  let m4: 'm4 = m2;
}

对于第二个示例,我认为我们可以走得更快一些并跳过一些步骤。与第一个示例一样,您可以立即选择 'x = 'outer。现在,您可能想做 'm1 = 'inner,但事实并非如此,因为 'm1 必须在分配 m2 时结束。 相反,应选择以下生命周期

'outer: {
  let mut x = Foo;
  let m2: 'm2;
  'inner: {
    let m1: 'm1 = &mut x;       // ------\ 
                                //       |- `'m1`
    m2 = &mut *m1;              // ------+
  }                             //       |- `'m2`
                                //       |
  let m4: 'm4 = m2;             // ------+
                                //       |- `'m4`
                                // ------/
}

在这种情况下,您注意到没有人使用 'inner 范围作为其生命周期范围,因此您实际上可以摆脱它。

'outer: {
  let mut x = Foo;
  let m2: 'm2;
  let m1: 'm1 = &mut x;       // ------\ 
                              //       |- `'m1`
  m2 = &mut *m1;              // ------+
                              //       |- `'m2`
  let m4: 'm4 = m2;           // ------+
                              //       |- `'m4`
                              // ------/
}

既然这样写,你也可以在定义的同时声明'm2

'outer: {
  let mut x = Foo;
  let m1: 'm1 = &mut x;       // ------\ 
                              //       |- `'m1`
  let m2: 'm2 = &mut *m1;     // ------+
                              //       |- `'m2`
  let m4: 'm4 = m2;           // ------+
                              //       |- `'m4`
                              // ------/
}

让我们去掉所有生命周期标记。

let mut x = Foo;
let m1 = &mut x;
let m2 = m1;
let m4 = m2;

瞧瞧!这实际上更接近 Rust 将 的事情(从某种意义上说,编译后的代码将要做什么)——假设这些变量交换不会被优化掉。此外,编译器现在知道每个变量的生命周期。

简短的结尾说明

请记住,这并不是编译器实际执行的操作。该算法更复杂,存在极端情况,我忽略了一些细节。但是,要点是您可以将其作为编译器功能的心智模型。它足够准确,可以理解编译错误,理解如何编写正确的代码或一开始就不正确的补丁代码,而且它足够简单,让您“运行”它而无需实际拿纸和一支笔(至少当你习惯了它)。