&'a Type<'a> 变成了永远借用

对生命周期有足够了结的 Rustancean 不会对以下代码感到不解。


#![allow(unused)]
fn main() {
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 Ty<'a> 牵绊你的时候 开始进入本文的正题。

&'a mut Ty<'a> 是一种反模式

你绝不应该写 &'a mut Ty<'a>,因为它代表永远借用自己 —— 被借用的对象活 'a 那么长,而你指定了借用必须活 'a

每当看到类似这种标注,都应该警惕这基本上是一个死胡同。因为一旦你获得 &'a mut Ty<'a>,那么面临两种选择

  1. 一直使用它,但它是 Ty<'a> 的全周期独占引用, Ty<'a> 活多长,这个独占引用就维持多长,借用规则让你永远不允许 存在另一个独占引用 (&'another mut Ty<'a>),也永远不允许有其他共享引用 (&'another Ty<'a>)。
    (但允许 reborrows (&'sub mut Ty<'a>),因为 &'a mut 中,&mut'a 协变,见下面的 永久借用中的协变
  2. 不再使用它,那么 &'a mut Ty<'a>'a 不再存活,也就是 Ty<'a> 不再存活,即无法再使用 Ty<'a>

所以,&'a mut Ty<'a> 这个独占引用变成对 Ty<'a> 的永久独占访问,似乎 Ty<'a> 的所有权也随着这个独占引用被夺走了: 你永远无法得到 Ty<'a> 的所有权,也永远无法转移 Ty<'a> 的所有权 —— 因为在 Rust 中移动一个值的前提是,这个值不被借用。

你还可以读读以下链接,通过具体代码去理解:

&'a Ty<'a> 通常不会牵绊住你

这得益于 covariance(协变),也就是生命周期可以缩短的能力。具体来说,因为 &'a T 具有两处协变:

  • &T 是协变的,即 &T 可以当作 &U 去使用,只要 TU 的子类型
  • &'a 是协变的,即 &'a 可以当作 &'b 去使用,只要 'a: 'b1'a'b 的子类型)

所以如果 Ty<'a>'a 也是协变的话2&'a Ty<'a> 可以先对 Ty 缩短成 &'a Ty<'b>,然后对 'a 缩短成 &'b Ty<'b>,从而每次使用 &'a Ty<'a>,都变成了临时的借用 &'b Ty<'b>,最终避免了永远借用 'a

1

'a: 'b'a outlives 'b,也就是 'a 至少和 'b 一样长,也就是 'a 活得和 'b 一样或者更长

2

对于上述 'a: 'b,有Ty<'a>: Ty<'b>,即 Ty<'a>Ty<'b> 的子类型。注意:严格来说,对生命周期使用 : 记号是符合 Rust 的,但对类型使用 : 记号(T: U),是不太规范的。

如果你对 Rust 中的 subtyping 和 variance 不熟悉,请阅读:

&'a Ty<'a> 牵绊你的时候

可以通过破坏 &'a Ty<'a> 进行协变的前提,来让 &'a Ty<'a> 绊倒你。

具体来说,如果 Ty<'a>'a 不再是协变,而是不变 (invariant),那么对于 'a: 'b

  • &'a Ty<'a> 依然可以缩短成 &'b Ty<'a> (但仅限在 'a 存活期间,见 永久借用中的协变
  • Ty<'a> 无法缩短成 Ty<'b>,从而 &'a Ty<'a>&'b Ty<'a> 无法缩短成 &'b Ty<'b>

实际上,&'a Ty<'a>&'a mut Ty<'a> 其实几乎一模一样 (playground for &'a mut Ty<'a>) ,唯一的区别在于一个是永久共享借用,另一个是永久独占借用。

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 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) {}
}

永久借用中的协变

引用的生命周期是协变的:对于 'a: 'b,任何 &'a&'a mut 都可以因为协变相应地缩短成 &'b&'b mut

生命周期是协变的(即生命周期可以缩短),而引用通常可以再借 (reborrow),这表明:一个长的生命周期,可以在它存活的状态中,被“分割”成互不相交的子生命周期。

这也适用于永久借用。以下两个代码展示了如何在永久借用的存活期间再借(重点在 borrow 内部)。

fn main() {
    let val = ();
    let mut ref_val = &val;
    let mut invariant = Invariant(&mut ref_val);
    invariant.borrow(); // Invariant<'a>::borrow(&'a mut Self)
    
    // error: cannot borrow `invariant` as mutable more than once at a time
    // 因为形成新的 &'another mut Ty<'a>,需要结束任何其他引用,而 &'a mut Ty<'a> 与 Ty<'a> 同生共死
    // invariant.temp_borrow(); 
}

struct Invariant<'a>(*mut &'a ()); // `*mut T` 中,`*mut` 对 T 不变
impl<'a> Invariant<'a> {
    fn borrow(&'a mut self) { // ok
        // 永久借用期间,进行多次 reborrows:&'temp (*(&'a mut self))
        // 长的生命周期被“分割”彼此成互不相交的子生命周期
        self.temp_borrow();
        self.temp_borrow();
        self.temp_borrow();
    }
    fn temp_borrow(&mut self) {}
    fn consume(self) {}
}
fn main() {
    let val = ();
    let mut ref_val = &val;
    let mut invariant = Invariant(&mut ref_val);
    invariant.borrow(); // Invariant<'a>::borrow(&'a Self)
    
    invariant.temp_borrow(); // ok: 共享借用可以共享同一个生命周期
    
    // error: cannot borrow `invariant` as mutable because it is also borrowed as immutable
    // 因为形成新的 &'another mut Ty<'a>,需要结束任何其他引用,而 &'a Ty<'a> 与 Ty<'a> 同生共死
    // invariant.temp_mut_borrow(); 
}

struct Invariant<'a>(*mut &'a ()); // `*mut T` 中,`*mut` 对 T 不变
impl<'a> Invariant<'a> {
    fn borrow(&'a self) { // ok
        // 思路一:
        // 永久借用期间,进行多次 reborrows:&'temp (*(&'a self))
        // 长的生命周期被“分割”彼此成互不相交的子生命周期 &'temp1、&'temp2、&'temp3
        
        // 思路二:共享借用可以共享同一个生命周期,以下都是 &'a self
        
        self.temp_borrow();
        self.temp_borrow();
        self.temp_borrow();
    }
    fn temp_borrow(&self) {}
    fn temp_mut_borrow(&mut self) {}
    fn consume(self) {}
}

附录:永远借用会如何绊住你的脚

与 drop check 交互


#![allow(unused)]
fn main() {
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
}
当 Invariant 自身或者内部需要 Drop 时,原本无 Drop 时能通过的代码,现在无法通过。

#![allow(unused)]
fn main() {
// 原本无 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) {} }
}

#![allow(unused)]
fn main() {
// 原本无 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

与生命周期标注交互

有时,你的代码没有出现显式的 &'a Ty<'a>,但依然有可能因为生命周期标注,让你隐式得到它。

正如前述所言,&'a Ty<'a>Ty<'a>'a 协变时,通常不会造成影响;但若对 'a 不变, &'a Ty<'a>&'a mut Ty<'a> 几乎会造成同样的麻烦(唯一区别在于,一个是永久共享借用,另一个是永久独占借用)。

永久借用意味着

  • Ty<'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) 的语法糖,但在这不是重点)。

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> 牵绊你的时候” 相同麻烦。

我自己推导生命周期会遵循一个套路,而第一步就是脱糖。f 和 g 两个函数的脱糖形式我已经写出来了,但它们的原型 在这,来自这个 帖子。(我知道帖子给的代码不是 Rust 惯用的代码,到处滥用了重置运算符和内存泄露,但这里的重点在于生命周期与型变,只需要看签名)

核心要点是每个方法调用变成最纯粹的形式,生命周期关系写得越清楚越好。我不会在这里描述具体怎么脱糖,这不是重点。

方法脱糖成函数也仅仅是个开始,接下来需要精简核心问题的代码。上面的代码已经是最能复现问题的精简版,具体过程也不是重点。

然后,一个核心步骤是,机械地写下源代码里每处相关的生命周期和类型,这基于你对生命周期的了解程度。

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()
}

而且编译器正确指明了那个永久借用发生的位置!


#![allow(unused)]
fn main() {
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
}
【点击展开】关于原帖,以及我猜来自原帖的读者会有的一些疑问

对原型的标注 在这(我依然简化了一些非常无关问题的代码)。

疑问1:不显式调用 drop(x) 不就可以通过代码,需要那么麻烦去弄清楚吗?

回答:这正是我在 与 drop check 交互 写的,你需要知道这样的代码不是真正有用的。 如果你阅读了全文,当你按照同样方式简单封装一下代码 (playground),就会充分理解编译器指出的问题 —— 两个 &'1 x 都被捕捉到了。


#![allow(unused)]
fn main() {
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> 中似乎有改进的空间


#![allow(unused)]
fn main() {
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)


#![allow(unused)]
fn main() {
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> 能够通过编译的原因。