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)
移动m1到m3会触发错误,因为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?这可以用寿命来“解释”,而实际上远不止于此。生命周期不仅仅是描述编译器执行的过于复杂的操作的便捷方式:它们是编译器用来处理所有权和借用的抽象。也就是说,编译器将计算每个变量的生命周期,因此它知道何时必须为这些变量释放内存。
\n让我们逐步了解如何在您的示例中执行此操作。请注意,这\'lifetime: { ... }不是有效的语法,但我们将使用该范围的生命周期来\'lifetime表示。同样,let a: \'a = ...不是有效的语法,但我们将用它来表示 a 的生命周期为\'a。
目标如下:如果我的生命周期绑定到一个范围,那么这是一个已知的生命周期,因为我知道它可以确保该生命周期的值存在多长时间。因此,我应该能够创建作用域,使变量的生命周期与作用域完全相同,并且尊重变量生命周期之间的关系。
\n我们首先命名每个生命周期。
\nstruct 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}\nRun Code Online (Sandbox Code Playgroud)\n我们可能看到的第一个关系是:
\n\'x: \'m1;\'x: \'m2;\'x: \'m3;\'x: \'m4。这些将被读取\'xoutlives\'mX,这意味着最终,\'x有效的范围必须包含\'mX\ 的范围。
其他关系有:
\n\'m3\'m1由于 的定义,必须从结束m3处开始\'m4必须从结束处开始。\'m2m2这些都是因为&mut T非Copy,所以m1必须m3移动。
其他约束是由于 Rust 的独占借用规则造成的,并且必须与\xe2\x89\xa0\'mX不相交。\'mYXY
而且,隐含地,\'outer它比一切都更长寿。在这个例子中,您可能会想到\'outeras \'static,因为唯一的要求是它\'static必须比所有东西都长寿。
现在我们已经枚举了约束条件,我们可以尝试求解问题,即“求解未知数\'x、\'m1、\'m2、\'m3” \'m4。第一步是为 分配一个生命周期\'x,因为这很容易。\'outer除了作用域的生命周期之外,它没有被任何人超越的限制,所以让我们分配\'x= \'outer。由于我们隐含的假设,这符合前四个约束,并且实际上使它们无效(根据 的定义它们是正确的\'outer,因此无需再担心)。
然后,我们要尝试找到满足移动条件的范围。
\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}\nRun Code Online (Sandbox Code Playgroud)\n我们在这里没有太多自由,因为我们被告知确切的时间\'m1、\'m2开始和结束。现在矛盾来了:\'m1和\'m2一定是不相交的,但它们显然不是!也就是说,您将无法找到\'x满足所有条件的范围,就像我们为 所做的那样。
让我们像在第一个示例中那样命名事物。
\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}\nRun Code Online (Sandbox Code Playgroud)\n对于第二个示例,我认为我们可以更快一点并跳过一些步骤。与第一个示例一样,您可以立即选择\'x= 。\'outer现在,您可能想做\'m1= \'inner,但事实并非如此,因为\'m1必须在分配时结束m2。\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}\nRun Code Online (Sandbox Code Playgroud)\n在这种情况下,您注意到没有人使用该\'inner范围作为其生命周期的范围,因此您实际上可以摆脱它。
\'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}\nRun Code Online (Sandbox Code Playgroud)\n既然是这样写的,你也可以声明\'m2定义的同时也可以声明了。
\'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}\nRun 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}\nRun Code Online (Sandbox Code Playgroud)\n等等\xc3\xa0!这实际上更接近 Rust 将要做的事情(从某种意义上说,编译后的代码将要做的事情)\xe2\x80\x94 假设这些变量交换不会得到优化。此外,编译器现在知道每个变量的生命周期。
\n请记住,这并不完全是编译器实际执行的操作。该算法更复杂,存在极端情况,并且我忽略了一些细节。然而,主要的一点是,您可以将其保留为编译器所做操作的心理模型。它足够准确,可以理解编译错误,了解如何编写正确的代码或修补最初不正确的代码,并且它足够简单,您可以“运行”它,而无需实际拿纸和一支笔(至少当你习惯了之后)。
\n| 归档时间: |
|
| 查看次数: |
135 次 |
| 最近记录: |