generated from zjp-CN/mdbook-template
-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
381 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,380 @@ | ||
# 当 `&'a Type<'a>` 变成了永远借用 | ||
|
||
对生命周期有足够了结的 Rustancean 不会对以下代码感到不解。 | ||
|
||
```rust | ||
struct Person<'a> { | ||
name: &'a str, | ||
} | ||
|
||
impl<'a> Person<'a> { | ||
// vvvvvvvv `&'a Person<'a>` | ||
fn borrow(&'a self) -> &'a str { | ||
self.name | ||
} | ||
|
||
// vvvvvvvvvvvv `&'a mut Person<'a>` | ||
fn borrow_forever(&'a mut self) -> &'a str { | ||
self.name | ||
} | ||
} | ||
|
||
fn fails(mut person: Person<'_>) { | ||
person.borrow_forever(); | ||
}// error: `person` dropped here while still borrowed | ||
|
||
fn works(mut person: Person<'_>) { | ||
let _one = person.borrow(); | ||
let _two = person.borrow(); | ||
&mut person; // ok | ||
} | ||
``` | ||
|
||
# `&'a mut Ty<'a>` 是一种反模式 | ||
|
||
你绝不应该写 `&'a mut Ty<'a>`,因为它代表永远借用自己 —— 被借用的对象活 `'a` 那么长,而你指定了借用必须活 `'a`。 | ||
|
||
每当看到类似这种标注,都应该警惕这基本上是一个死胡同。因为一旦你获得 `&'a mut Ty<'a>`,那么面临两种选择 | ||
1. 一直使用它,但它是 `Ty<'a>` 的全周期独占引用, `Ty<'a>` 活多长,这个独占引用就维持多长,借用规则让你永远不允许 | ||
reborrow (`&'another mut Ty<'a>`),也永远不允许有共享引用 (`&'another Ty<'a>`)。 | ||
2. 不再使用它,那么 `&'a mut Ty<'a>` 的 `'a` 不再存活,也就是 `Ty<'a>` 不再存活,即无法再使用 `Ty<'a>`。 | ||
|
||
所以,`&'a mut Ty<'a>` 这个独占引用变成对 `Ty<'a>` 的永久独占访问,似乎 `Ty<'a>` 的所有权也随着这个独占引用被夺走了: | ||
你永远无法得到 `Ty<'a>` 的所有权,也永远无法转移 `Ty<'a>` 的所有权 —— 因为在 Rust 中移动一个值的前提是,这个值不被借用。 | ||
|
||
你还可以读读以下链接,通过具体代码去理解: | ||
|
||
* [自引用与生命周期](https://zjp-cn.github.io/translation/lifetime/self-referential.html#%E4%B8%80%E7%A7%8D%E5%8F%AF%E8%A1%8C%E4%BD%86%E6%97%A0%E7%94%A8%E5%81%9A%E6%B3%95) | ||
* [Borrowing something forever](https://quinedot.github.io/rust-learning/pf-borrow-forever.html) | ||
|
||
# `&'a Ty<'a>` 通常不会牵绊住你 | ||
|
||
这得益于 covariance(协变),也就是生命周期可以缩短的能力。具体来说,因为 `&'a T` 具有两处协变: | ||
* `&` 对 `T` 是协变的,即 `&T` 可以当作 `&U` 去使用,只要 `T` 是 `U` 的子类型 | ||
* `&` 对 `'a` 是协变的,即 `&'a` 可以当作 `&'b` 去使用,只要 `'a: 'b`[^outlive](`'a` 是 `'b` 的子类型) | ||
|
||
所以**如果 `Ty<'a>` 对 `'a` 也是协变的话**[^subtype], `&'a Ty<'a>` 可以先对 `Ty` 缩短成 `&'a Ty<'b>`,然后对 `'a` | ||
缩短成 `&'b Ty<'b>`,从而每次使用 `&'a Ty<'a>`,都变成了临时的借用 `&'b Ty<'b>`,最终避免了永远借用 `'a`。 | ||
|
||
[^outlive]: `'a: 'b` 指 `'a` outlives `'b`,也就是 `'a` 至少和 `'b` 一样长,也就是 `'a` 活得和 `'b` 一样或者更长 | ||
|
||
[^subtype]: 对于上述 `'a: 'b`,有`Ty<'a>: Ty<'b>`,即 `Ty<'a>` 是 `Ty<'b>` 的子类型。注意:严格来说,对生命周期使用 | ||
`:` 记号是符合 Rust 的,但对类型使用 `:` 记号(`T: U`),是不太规范的。 | ||
|
||
如果你对 Rust 中的 subtyping 和 variance 不熟悉,请阅读: | ||
* [Nomicon: subtyping](https://doc.rust-lang.org/nomicon/subtyping.html) | ||
* [我的笔记](https://zjp-cn.github.io/rust-note/subtyping.html) | ||
|
||
|
||
# 当 `&'a Ty<'a>` 牵绊你的时候 | ||
|
||
可以通过破坏 `&'a Ty<'a>` 进行协变的前提,来让 `&'a Ty<'a>` 绊倒你。 | ||
|
||
具体来说,如果 `Ty<'a>` 对 `'a` 不再是协变,而是不变 (invariant),那么对于 `'a: 'b` | ||
* `&'a Ty<'a>` 依然可以缩短成 `&'b Ty<'a>` | ||
* 只是 `Ty<'a>` 无法缩短成 `Ty<'b>`,从而 `&'a Ty<'a>` 和 `&'b Ty<'a>` 无法缩短成 `&'b Ty<'b>` | ||
|
||
很多情况下,这不会造成什么问题,因为你依然可以缩短生命周期得到临时的 `&'b Ty<'a>`,避免了永久借用。 | ||
|
||
但随着代码变得复杂,你可能在不知不觉中重蹈 `&'a mut Ty<'a>` 的覆辙。考虑以下代码: | ||
|
||
```rust,editable | ||
fn main() { | ||
let val = (); | ||
let mut ref_val = &val; | ||
let mut invariant = Invariant(&mut ref_val); | ||
invariant.borrow(); // Invariant<'a>::borrow(&'a Self) | ||
invariant.borrow(); // ok: 你总是可以有多个 &'a T | ||
// 但你不能做以下事情中的任何一件 | ||
// 但不能有 &'a mut T 和 &'a T 同时存在 | ||
&mut invariant; // error: cannot borrow `invariant` as mutable because it is also borrowed as immutable | ||
// 没法 move:因为 move 一个变量的前提是这个变量不被借用 | ||
let _move = invariant; // error: cannot move out of `invariant` because it is borrowed | ||
// 没法显式调用 drop(和按值方式接收参数的函数):理由同“没法 move” | ||
invariant.consume(); | ||
drop(invariant); // error: cannot move out of `invariant` because it is borrowed | ||
} | ||
struct Invariant<'a>(*mut &'a ()); // `*mut T` 中,`*mut` 对 T 不变 | ||
impl<'a> Invariant<'a> { | ||
fn borrow(&'a self) {} | ||
fn consume(self) {} | ||
} | ||
``` | ||
|
||
# 附录:永远借用会如何绊住你的脚 | ||
|
||
## 与 drop check 交互 | ||
|
||
```rust | ||
struct Invariant<'a>(*mut &'a ()); // 自身及其内部无需 Drop | ||
impl<'a> Invariant<'a> { fn borrow(&'a self) {} } | ||
|
||
// 在函数内创建无 Drop 的 Invariant 并永远借用 | ||
// (这可能是你写 Rust 的第一步,代码示例成功编译) | ||
fn ok() { | ||
let val = (); | ||
let mut ref_val = &val; | ||
let mut invariant = Invariant(&mut ref_val); | ||
|
||
invariant.borrow(); | ||
} | ||
// 你以为这样就没问题?看下面 fail 的情况 | ||
|
||
// (你想对一段代码进行封装,却发现无法编译) | ||
// 将所有权移入函数(在函数外创建 Invariant),并在函数内永远借用 | ||
// error: `val` does not live long enough | ||
fn fail(val: Invariant<'_>) { | ||
val.borrow(); | ||
} // `val` dropped here while still borrowed | ||
``` | ||
|
||
<details> | ||
<summary>当 Invariant 自身或者内部需要 Drop 时,原本无 Drop 时能通过的代码,现在无法通过。</summary> | ||
|
||
```rust | ||
// 原本无 Drop 时能通过的代码,现在无法通过 | ||
// error: `invariant` does not live long enough | ||
fn fail() { | ||
let val = (); | ||
let mut ref_val = &val; | ||
let mut invariant = Invariant(&mut ref_val); | ||
|
||
invariant.borrow(); | ||
} // `invariant` dropped here while still borrowed | ||
// borrow might be used here, when `invariant` is dropped and | ||
// runs the `Drop` code for type `Invariant` | ||
|
||
struct Invariant<'a>(*mut &'a ()); | ||
impl<'a> Invariant<'a> { fn borrow(&'a self) {} } | ||
|
||
// 当 Invariant 自身需要 Drop | ||
impl Drop for Invariant<'_> { fn drop(&mut self) {} } | ||
``` | ||
|
||
```rust | ||
// 原本无 Drop 时能通过的代码,现在无法通过 | ||
// error: `invariant` does not live long enough | ||
fn fail() { | ||
let val = (); | ||
let mut ref_val = &val; | ||
let invariant = Invariant(Inner(&mut ref_val)); | ||
invariant.borrow(); | ||
} // 同 `当 Invariant 自身需要 Drop` | ||
|
||
struct Invariant<'a>(Inner<'a>); | ||
impl<'a> Invariant<'a> { | ||
fn borrow(&'a self) {} | ||
} | ||
|
||
// 当 Invariant 内部需要 Drop | ||
struct Inner<'a>(*mut &'a ()); | ||
impl Drop for Inner<'_> { fn drop(&mut self) {} } | ||
``` | ||
|
||
对这些情况的解释见 [Nomicon: drop check](https://doc.rust-lang.org/nomicon/dropck.html)。 | ||
</details> | ||
|
||
|
||
## 与生命周期标注交互 | ||
|
||
有时,你的代码没有出现显式的 `&'a Ty<'a>`,但依然有可能因为生命周期标注,让你隐式得到它。 | ||
|
||
正如前述所言,`&'a Ty<'a>` 在 `Ty<'a>` 对 `'a` 协变时,通常不会造成影响;但若对 `'a` 不变, | ||
`&'a Ty<'a>` 与 `&'a mut Ty<'a>` 几乎造成同样的困难(唯一区别在于,一个是永久共享借用,另一个是永久独占借用)。 | ||
|
||
永久借用意味着 | ||
* `Ty<'a>` 与这个永久借用生死与共:要么一起存活,要么一起死亡 | ||
* `Ty<'a>` 这个值一直被借用:所有权无法被获得和转移 | ||
|
||
在下述例子中,代码没有显式的 `&'a Ty<'a>` 或 `&'a mut Ty<'a>` (严格来说,其实存在 `&'a mut Ty<'a>`,因为 | ||
`&'a mut dyn std::fmt::Debug` 其实是 `&'a mut dyn ('a + std::fmt::Debug)` 的语法糖,但在这不是重点)。 | ||
|
||
```rust,editable | ||
use std::cell::RefCell; | ||
fn main() { | ||
let mut s1 = String::from(""); | ||
let mut ss = &mut s1; | ||
let mut x = MyData(RefCell::new(&mut ss)); | ||
let y = f(&x, &x); | ||
g(y, &x); | ||
&x; // ok | ||
// &mut x; // error: cannot borrow `x` as mutable because it is also borrowed as immutable | ||
// drop(x);// error: cannot move out of `x` because it is borrowed | ||
} | ||
struct MyData<'a>(RefCell<&'a mut dyn std::fmt::Debug>); | ||
fn static_data<'any>() -> MyData<'any> { | ||
MyData(RefCell::new(Box::leak(Box::new("")))) | ||
} | ||
fn f<'a, 'b>(_: &'b MyData<'a>, _: &'b MyData<'a>) -> MyData<'b> { | ||
static_data() | ||
} | ||
fn g<'a, 'b>(_: MyData<'a>, _: &'b MyData<'a>) -> MyData<'b> { | ||
static_data() | ||
} | ||
``` | ||
|
||
但实际上是存在一个隐式的 `&'a Ty<'a>`,且 `Ty<'a>` 对 `'a` 不变,从而遇到与 | ||
[“当 &'a Ty<'a> 牵绊你的时候”](#当-a-tya-牵绊你的时候) 相同麻烦。 | ||
|
||
我自己推导生命周期会遵循一个套路,而第一步就是脱糖。f 和 g 两个函数的脱糖形式我已经写出来了,但它们的原型 | ||
[在这](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=497b781639a77c058ec2075fa4568b0f),来自这个 | ||
[帖子](https://rustcc.cn/article?id=431b5cac-51db-470e-ba17-3f29344eb672)。(我知道帖子给的代码不是 Rust | ||
惯用的代码,到处滥用了重置运算符和内存泄露,但这里的重点在于生命周期与型变,只需要看签名) | ||
|
||
核心要点是每个方法调用变成最纯粹的形式,生命周期关系写得越清楚越好。我不会在这里描述具体怎么脱糖,这不是重点。 | ||
|
||
方法脱糖成函数也仅仅是个开始,接下来需要精简核心问题的代码。上面的代码已经是最能复现问题的精简版,具体过程也不是重点,无需赘述。 | ||
|
||
然后,一个核心步骤是,机械地写下源代码里每处相关的生命周期和类型,这基于你对生命周期有多了解。 | ||
|
||
```rust,editable | ||
use std::cell::RefCell; | ||
fn main() { | ||
let mut s1 = String::from(""); | ||
let mut ss = &mut s1; // ss: &'0 mut String | ||
// ss => &'0 mut String => &'1 mut String (协变, '1 来自 &'1 mut ss) | ||
// &'1 mut ss => &'1 mut &'1 mut String => &'1 mut dyn ('1 + Debug) | ||
// x: MyData(RefCell<'&'1 mut dyn Debug>) | ||
let mut x = MyData(RefCell::new(&mut ss)); // x: MyData<'1> (不变, '1 无法缩短) | ||
let y = f(&x, &x); // f(&'2 MyData<'1>, &'2 MyData<'1>) -> MyData<'2> ('2 来自 &'2 x) | ||
g(y, &x); // g(MyData<'2>, &'3 MyData<'1>) -> MyData<'3> (注意:这直接将 y 的类型代入) | ||
// 显然 y 的类型上的生命周期与 g 的签名上的不一致:y 与 x 在类型上具有相同的生命周期。 | ||
// 而 x 的生命周期 '1 无法缩短,从而试着把 '2 = '1 代入,得到 | ||
// g(MyData<'1>, &'3 MyData<'1>) -> MyData<'3> 符合 g 的签名。 | ||
// 倒推 f(&'1 MyData<'1>, &'1 MyData<'1>) -> MyData<'1>,嗯,看见 &'1 MyData<'1> (即 &'1 x) 了吗, | ||
// &'1 x 是一个永久借用! | ||
drop(x);// error: cannot move out of `x` because it is borrowed | ||
} | ||
struct MyData<'a>(RefCell<&'a mut dyn std::fmt::Debug>); | ||
fn static_data<'any>() -> MyData<'any> { | ||
MyData(RefCell::new(Box::leak(Box::new("")))) | ||
} | ||
fn f<'a, 'b>(_: &'b MyData<'a>, _: &'b MyData<'a>) -> MyData<'b> { | ||
static_data() | ||
} | ||
fn g<'a, 'b>(_: MyData<'a>, _: &'b MyData<'a>) -> MyData<'b> { | ||
static_data() | ||
} | ||
``` | ||
|
||
而且编译器正确指明了那个永久借用发生的位置! | ||
|
||
```rust | ||
error[E0505]: cannot move out of `x` because it is borrowed | ||
--> src/main.rs:20:10 | ||
| | ||
9 | let mut x = MyData(RefCell::new(&mut ss)); | ||
| ----- binding `x` declared here | ||
10 | | ||
11 | let y = f(&x, &x); | ||
| -- borrow of `x` occurs here | ||
... | ||
20 | drop(x); | ||
| ^ | ||
| | | ||
| move out of `x` occurs here | ||
| borrow later used here | ||
``` | ||
|
||
<details> | ||
<summary>关于原帖,以及我猜来自原帖的读者会有的一些疑问</summary> | ||
|
||
对原型的标注 [在这](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=ba2376e9eaef0e5b2dd7305d6b259e19)(我依然简化了一些非常无关问题的代码)。 | ||
|
||
疑问1:不显式调用 `drop(x)` 不就可以通过代码,需要那么麻烦去弄清楚吗? | ||
|
||
回答:这正是我在 [与 drop check 交互](#与-drop-check-交互) 写的,你需要知道这样的代码不是真正有用的。 | ||
如果你阅读了全文,当你按照同样方式简单封装一下代码 | ||
([playground][mydata]),就会充分理解编译器指出的问题 —— 两个 `&'1 x` 都被捕捉到了。 | ||
|
||
[mydata]: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=aae44ae01b5ab0940ad6688ad37513d1 | ||
|
||
```rust,no_run | ||
fn fail(x: MyData<'_>) { | ||
let y = &x + &x; | ||
let _ = y + &x; | ||
} | ||
error[E0597]: `x` does not live long enough | ||
--> src/main.rs:71:13 | ||
| | ||
70 | fn fail(x: MyData<'_>) { | ||
| - | ||
| | | ||
| binding `x` declared here | ||
| has type `MyData<'1>` | ||
71 | let y = &x + &x; | ||
| ^^----- | ||
| | | ||
| borrowed value does not live long enough | ||
| assignment requires that `x` is borrowed for `'1` | ||
72 | let _ = y + &x; | ||
73 | } | ||
| - `x` dropped here while still borrowed | ||
error[E0597]: `x` does not live long enough | ||
--> src/main.rs:71:18 | ||
| | ||
70 | fn fail(x: MyData<'_>) { | ||
| - | ||
| | | ||
| binding `x` declared here | ||
| has type `MyData<'1>` | ||
71 | let y = &x + &x; | ||
| -----^^ | ||
| | | | ||
| | borrowed value does not live long enough | ||
| assignment requires that `x` is borrowed for `'1` | ||
72 | let _ = y + &x; | ||
73 | } | ||
| - `x` dropped here while still borrowed | ||
``` | ||
|
||
疑问2:如何真正解决问题? | ||
|
||
原帖当然在滥用生命周期、滥用运算符、滥用内存泄露、滥用内部可变性,不应该那样过度设计程序。 | ||
|
||
此外,`&'a Invariant<'a>` 是我们需要极力避免的,对于简化后的代码,把 f 和 g | ||
函数单独看签名似乎都没有过度约束,结合起来形成了过度约束。所以型变中,对于 invariance | ||
是最需要注意的。重新回到出错的地方,我们会注意到有一个 `'3`,它在 `&'3 MyData<'1>` 中似乎有改进的空间 | ||
|
||
```rust,no_run | ||
let y = f(&x, &x); // f(&'2 MyData<'1>, &'2 MyData<'1>) -> MyData<'2> ('2 来自 &'2 x) | ||
g(y, &x); // g(MyData<'2>, &'3 MyData<'1>) -> MyData<'3> (注意:这直接将 y 的类型代入) | ||
``` | ||
|
||
当 `'3 = '2` 时,这两行的所有 `&x` 被变成了 `&'2 x`,这是合理的,因为共享借用可以共享同一个生命周期,从而有 | ||
([playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=c381fa2d1e927e20b0b42868329c9ecf)) | ||
|
||
```rust,no_run | ||
let y = f(&x, &x); // f(&'2 MyData<'1>, &'2 MyData<'1>) -> MyData<'2> ('2 来自 &'2 x) | ||
g(y, &x); // g(MyData<'2>, &'2 MyData<'1>) -> MyData<'2> | ||
// 相应的 g 的签名应改为 | ||
fn g<'a, 'b>(_: MyData<'b>, _: &'b MyData<'a>) -> MyData<'b> { ... } | ||
``` | ||
|
||
这就是原帖中,yuyidegit 给的 [`impl<'a, 'b> Add<&'b MyData<'a>> for MyData<'b>`][yuyidegit] 能够通过编译的原因。 | ||
|
||
[yuyidegit]: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=56a6eba5928d4d7e2fe6bd163c7dca04 | ||
|
||
</details> | ||
|
||
|
||
|