Skip to content

Commit

Permalink
add: covariance-borrow-forever
Browse files Browse the repository at this point in the history
  • Loading branch information
zjp-CN committed Dec 15, 2023
1 parent e7af1fd commit 1c65bc2
Show file tree
Hide file tree
Showing 2 changed files with 381 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
- [“变长参数”函数与回调](./dcl/variadic.md)
- [non-lexical lifetimes (NLL)]()
- [Subtyping and Variance](./subtyping.md)
- [`&'a Ty<'a>` 变成永远借用](./variance/covariance-borrow-forever.md)
- [官方论坛帖子整理](./forum.md)
- [从同质 variants 取同类型数据](./forum/homo-variant.md)
- [常量泛型参数的分类实现](./forum/impl-const-param.md)
Expand Down
380 changes: 380 additions & 0 deletions src/variance/covariance-borrow-forever.md
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>



0 comments on commit 1c65bc2

Please sign in to comment.