< 返回版块

Pikachu 发表于 2022-08-08 04:31

故事开始于论坛上的一个帖子。我这里尝试梳理一下自己的理解,麻烦大佬们帮忙看看有没有错误的地方。

https://rustcc.cn/article?id=7599eb03-76d7-42f2-9411-3b7fad3d8d95

这里简单概括一下令人困惑的代码。rust playground

struct X<'x> {
    y: &'x i32,
}

fn borrow<'a>(_: &'a X<'a>) {}

fn borrow_mut<'a>(_: &'a mut X<'a>) {}

fn main() {
    let y = 0i32;
    let mut x = X { y: &y };

    // example 1: this part can compile
    borrow(&x);
    let _z = &mut x.y;

    // example 2: this part cannot compile
    borrow_mut(&mut x);
    let _z = &x.y;
    
    // example 3: this part also compile
    let _z = &x.y;
    borrow_mut(&mut x);
}

在这里,&'a X<'a>中的'a看起来只占据了borrow所在的一行,而&'a mut X<'a>中的'a却是从borrow_mut这一行开始一直到最后一次使用x


在尝试阅读了一些资料(rustnomicon)之后,我发现区分example 1和example 2的关键似乎在于“协变”和“不变”上。就生命周期的问题而言,“协变”的意思是在一个需要短生命周期的地方,允许传入比它更长的生命周期;而“不变”则是指传入的生命周期必须与接收的生命周期相同。

一个不可变借用&'b T,它对'b是协变的,对T也是协变的(这里的T可能是含有生命周期的类型)。而一个可变借用&'b mut T,它对'b是协变的,但对T却是不变的。这样设计的理由是,试图缩短&'b mut T<'a>'a的大小,显然可能导致两个&'b mut T<'a>重叠却无法被检查出来。最后,结构体的“可变性”取决于它内部各个成员的“可变性”。

在上面的example 1中,borrow(&x)尝试把&'b X<'x>传递给一个&'a X<'a>的类型。这里产生的约束条件如下。('b: 'a表示'b至少和'a一样长)

'b: 'a
'x: 'a

这样可以找到最小的满足条件的'b就是borrow(&x)所在的那一行。

而在example 2中,borrow_mut(&x)尝试把&'b mut X<'x>传递给一个&'a mut X<'a>的类型。因为&'b mut TT是不变的,所以最终产生的约束条件是

'b: 'a
'a: 'x
'x: 'a

也就是说,'a'x'一样长,而'b至少要和'a一样长。而因为&'b mut X<'x>是对X<'x>的借用,所以'x也至少要和'b一样长。最终结果是,这里的出现的三个生命周期都相等。也就是说,这个可变借用的生命周期一直持续到X最后一次被使用的位置。这也就是example 2无法被编译通过的原因。


example 2和example 3的区别似乎主要在于NLL对生命周期约束的处理方面。NLL生成的约束条件,其实并不是简单的“'b至少和'a一样长”,而是“在代码的某一行P上,'b至少和'a一样长”。但更深入的原因我还没有彻底理解,希望有大佬补充吧。

评论区

写评论
苦瓜小仔 2022-08-08 13:04

你的理解基本是对的。

NLL 的基本资料是 RFC 2094: NLL,我翻译过,你可以看看。

每个引用都有自己的生命周期,而且,每次生成的一个引用,其生命周期结束于最后使用的地方。

fn main() {
    let y = 0i32;
    let mut x = X { y: &'y y }; // X<'y>

    // example 1: this part can compile
    borrow(&'b1 x); // &'y X<'y> -> &'b1 X<'y> (因为 & 对 &'any 中的 'any 协变) -> &'b1 X<'b1> (因为 &X 对 X 协变)
    let _z = &mut x.y; // &'y mut &'y i32 -> &'m1 mut &'y i32(因为 &'any mut X 对 'any 协变的)

    // example 2: this part cannot compile
    borrow_mut(&mut x); // &'y mut X<'y>(虽然 &'any mut X 对 'any 协变,但 borrow_mut 的标注阻止了这种协变,所以意味着变量 x 一直被自己借用)
    let _z = &x.y;
}

&'y mut X<'y>这里 一样:

这确实是可行的,但创建的值高度受制

因此 example3 虽然是编译通过的,但其使用是严格受限的:你无法在这个函数之外再使用变量 x 了。

虽然你可以维持这个引用来让继续使用 x,比如 fn borrow_mut<'a>(x: &'a mut X<'a>) -> &'a mut X<'a> { x }

但显然在真正的使用场景下,这不会是你想要的:playground

解决方法很简单,&'y mut X<'y> 其实可以缩短成 &'m mut X<'y>,所以对 borrow_mut 的生命周期标注放松:

  • fn borrow_mut<'a, 'b: 'a>(_: &'a mut X<'b>)
  • 或者等价地 fn borrow_mut<'a, 'b>(_: &'a mut X<'b>)
  • 或者等价地 fn borrow_mut<'a>(_: &'a mut X<'_>)
  • 或者等价地 fn borrow_mut<'a>(_: &'a mut X)
  • 或者等价地 fn borrow_mut(_: &mut X<'_>)
  • 或者等价地 fn borrow_mut(_: &mut X)

修复我给的例子:playgound


多说一句:你注意到了,结构体的“可变性”取决于它内部各个成员的“可变性”,这其实也很重要。

因为并不是所有的 fn borrow<'a>(_: &'a X<'a>) 就一定能编译通过,考虑这个例子

playground

这里的推理过程和我说的一样,所以解决方法也是一样的 fn borrow(_: &X)

1 共 1 条评论, 1 页