"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
会触发错误,因为m1
被m2
借用了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 的情况下,一个人最终有两个可变借用到同一个变量,这是不合理的。在退出范围的情况下,可变借用无论如何都会死亡,并且重新借用仍然指向有效数据。
但我想知道:
- 如何根据生命周期和类型规则来解释这种行为?
- 在哪里可以找到有关详细行为的相关信息?它们存在于文档中,还是我应该参考
rustc
? 的源代码
这可以用生命周期来“解读”,其实远不止于此。生命周期不仅仅是描述编译器完成的过于复杂的操作的便捷方式:它们 是 编译器用来处理所有权和借用的抽象。也就是说,编译器将计算 每个 变量的生命周期,因此它知道何时必须为这些变量释放内存。
让我们逐步了解它如何在您的示例中执行此操作。请注意,'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 T
非Copy
,所以m1
和m3
必须移动
其他约束是由于 Rust 的独占借用规则,'mX
必须与 'mY
不相交,因为 X
≠ Y
。
此外,隐含地,'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 将 做 的事情(从某种意义上说,编译后的代码将要做什么)——假设这些变量交换不会被优化掉。此外,编译器现在知道每个变量的生命周期。
简短的结尾说明
请记住,这并不是编译器实际执行的操作。该算法更复杂,存在极端情况,我忽略了一些细节。但是,要点是您可以将其作为编译器功能的心智模型。它足够准确,可以理解编译错误,理解如何编写正确的代码或一开始就不正确的补丁代码,而且它足够简单,让您“运行”它而无需实际拿纸和一支笔(至少当你习惯了它)。
如果我理解正确的话,在 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
会触发错误,因为m1
被m2
借用了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 的情况下,一个人最终有两个可变借用到同一个变量,这是不合理的。在退出范围的情况下,可变借用无论如何都会死亡,并且重新借用仍然指向有效数据。 但我想知道:
- 如何根据生命周期和类型规则来解释这种行为?
- 在哪里可以找到有关详细行为的相关信息?它们存在于文档中,还是我应该参考
rustc
? 的源代码
这可以用生命周期来“解读”,其实远不止于此。生命周期不仅仅是描述编译器完成的过于复杂的操作的便捷方式:它们 是 编译器用来处理所有权和借用的抽象。也就是说,编译器将计算 每个 变量的生命周期,因此它知道何时必须为这些变量释放内存。
让我们逐步了解它如何在您的示例中执行此操作。请注意,'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
必须在'm1
结束的地方开始- 类似地,
'm4
必须从'm2
结束的地方开始,因为m2
. 的定义
m3
; 的定义,这些是由于&mut T
非Copy
,所以m1
和m3
必须移动
其他约束是由于 Rust 的独占借用规则,'mX
必须与 'mY
不相交,因为 X
≠ Y
。
此外,隐含地,'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 将 做 的事情(从某种意义上说,编译后的代码将要做什么)——假设这些变量交换不会被优化掉。此外,编译器现在知道每个变量的生命周期。
简短的结尾说明
请记住,这并不是编译器实际执行的操作。该算法更复杂,存在极端情况,我忽略了一些细节。但是,要点是您可以将其作为编译器功能的心智模型。它足够准确,可以理解编译错误,理解如何编写正确的代码或一开始就不正确的补丁代码,而且它足够简单,让您“运行”它而无需实际拿纸和一支笔(至少当你习惯了它)。