在 Rust 中,“退出范围”和“被移动”在重新借用时的表现有何不同?

Gue*_*0x0 1 rust borrow-checker

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

let mut x = ...;
let m1 = &mut x;
let m2 = &mut *m;
let m3 = m1; // m1 moved here
let m4 = m2; // use m2 here
Run Code Online (Sandbox Code Playgroud)

移动m1m3会触发错误,因为m1是 借用的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
Run Code Online (Sandbox Code Playgroud)

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

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

  • 如何根据生命周期和打字规则来解释这种行为?
  • 我在哪里可以找到有关详细行为的相关信息?它们是否存在于文档中,或者我应该参考 的源代码rustc

Bla*_*ans 5

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

\n

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

\n

目标如下:如果我的生命周期绑定到一个范围,那么这是一个已知的生命周期,因为我知道它可以确保该生命周期的值存在多长时间。因此,我应该能够创建作用域,使变量的生命周期与作用域完全相同,并且尊重变量生命周期之间的关系。

\n

第一个例子

\n

我们首先命名每个生命周期。

\n
struct Foo; // Does not implement `Copy`\n\n\'outer: {           // Code has to live in a scope (it might be `main`, for example)\n  let mut x: \'x = Foo;\n  let m1: \'m1 = &mut x;\n  let m2: \'m2 = &mut *m1;\n  let m3: \'m3 = m1; // m1 moved here\n  let m4: \'m4 = m2; // use m2 here\n}\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n
    \n
  • \'x: \'m1;
  • \n
  • \'x: \'m2;
  • \n
  • \'x: \'m3;
  • \n
  • \'x: \'m4
  • \n
\n

这些将被读取\'xoutlives\'mX,这意味着最终,\'x有效的范围必须包含\'mX\ 的范围。

\n

其他关系有:

\n
    \n
  • \'m3\'m1由于 的定义,必须从结束m3处开始
  • \n
  • 同样,由于 的定义,\'m4必须从结束处开始。\'m2m2
  • \n
\n

这些都是因为&mut TCopy,所以m1必须m3移动。

\n

其他约束是由于 Rust 的独占借用规则造成的,并且必须与\xe2\x89\xa0\'mX不相交。\'mYXY

\n

而且,隐含地,\'outer它比一切都更长寿。在这个例子中,您可能会想到\'outeras \'static,因为唯一的要求是它\'static必须比所有东西都长寿。

\n

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

\n

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

\n
\'outer: {\n  let mut x: \'x = Foo;\n  let m1: \'m1 = &mut x;               // ---------\\\n                                      //          |- `\'m1`\n  let m2: \'m2 = &mut *m1;             // ---------+--\\\n                                      //          |  |\n  let m3: \'m3 = m1;                   // ---------/  |- `\'m2`\n                                      //             |\n  let m4: \'m4 = m2;                   // ------------/\n}\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n

第二个例子

\n

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

\n
\'outer: {\n  let mut x: \'x = Foo;\n  let m2: \'m2;\n  \'inner: {\n    let m1: \'m1 = &mut x;\n    m2 = &mut *m1;\n  }\n  let m4: \'m4 = m2;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n
\'outer: {\n  let mut x: \'x = Foo;\n  let m2: \'m2;\n  \'inner: {\n    let m1: \'m1 = &mut x;\n    m2 = &mut *m1;\n  }\n  let m4: \'m4 = m2;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n
\'outer: {\n  let mut x = Foo;\n  let m2: \'m2;\n  \'inner: {\n    let m1: \'m1 = &mut x;       // ------\\ \n                                //       |- `\'m1`\n    m2 = &mut *m1;              // ------+\n  }                             //       |- `\'m2`\n                                //       |\n  let m4: \'m4 = m2;             // ------+\n                                //       |- `\'m4`\n                                // ------/\n}\n
Run Code Online (Sandbox Code Playgroud)\n

既然是这样写的,你也可以声明\'m2定义的同时也可以声明了。

\n
\'outer: {\n  let mut x = Foo;\n  let m2: \'m2;\n  let m1: \'m1 = &mut x;       // ------\\ \n                              //       |- `\'m1`\n  m2 = &mut *m1;              // ------+\n                              //       |- `\'m2`\n  let m4: \'m4 = m2;           // ------+\n                              //       |- `\'m4`\n                              // ------/\n}\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n
\'outer: {\n  let mut x = Foo;\n  let m1: \'m1 = &mut x;       // ------\\ \n                              //       |- `\'m1`\n  let m2: \'m2 = &mut *m1;     // ------+\n                              //       |- `\'m2`\n  let m4: \'m4 = m2;           // ------+\n                              //       |- `\'m4`\n                              // ------/\n}\n
Run Code Online (Sandbox Code Playgroud)\n

等等\xc3\xa0!这实际上更接近 Rust 将要做的事情从某种意义上说,编译后的代码将要做的事情)\xe2\x80\x94 假设这些变量交换不会得到优化。此外,编译器现在知道每个变量的生命周期。

\n

简短的结束语

\n

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

\n