故事开始于论坛上的一个帖子。我这里尝试梳理一下自己的理解,麻烦大佬们帮忙看看有没有错误的地方。
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 T
对T
是不变的,所以最终产生的约束条件是
'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
一样长”。但更深入的原因我还没有彻底理解,希望有大佬补充吧。
评论区
写评论你的理解基本是对的。
NLL 的基本资料是 RFC 2094: NLL,我翻译过,你可以看看。
每个引用都有自己的生命周期,而且,每次生成的一个引用,其生命周期结束于最后使用的地方。
&'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)
。