原文链接:https://doc.rust-lang.org/nomicon/lifetimes.html
生命周期
Rust在整个生命周期里强制执行生命周期的规则。
生命周期是引用必须有效的命名代码区域。这些区域可能相当复杂,因为它们对应于程序中的执行路径。在这些执行路径上甚至可能存在漏洞,因为只要在重新使用引用之前对其进行了初始化,就有可能使引用无效。包含引用(或假装为引用)的类型也可以用生命周期进行标记,以便 Rust 可以防止它们变无效。
在我们的大多数示例中,生命周期将与作用域一致。这是因为我们的示例很简单。它们不一致的更为复杂的情况如下所述。
在函数体内,Rust通常不需要你显式地给生命周期起名字。这是因为在本地上下文里,一般没有必要关注生命周期。Rust知道程序的全部信息,从而可以完美地执行各种操作。它可能会引入许多匿名或者临时的作用域让程序顺利执行。
但是如果你要跨出函数的边界,就需要关心生命周期了。生命周期用这样的符号表示:'a
,'static
。为了更清晰地了解生命周期,我们假设我们可以为生命周期打标签,去掉本章所有例子的语法糖。
最开始,我们的示例代码对作用域和生命周期使用了很激进的语法糖特性——甜得像玉米糖浆一样,因为把所有的东西都显式地写出来实在很讨厌。所有的Rust代码都采用比较激进的理论以省略“显而易见”的东西。
一个特别有意思的语法糖是,每一个let
表达式都隐式引入了一个作用域。大多数情况下,这一点并不重要。但是当变量之间互相引用的时候,这就很重要了。举个简单的例子,我们彻底去掉下面这段代码的语法糖:
#![allow(unused)] fn main() { let x = 0; let y = &x; let z= &y; }
借用检查器通常会尽可能减少生命周期的范围,所以去掉语法糖后的代码大概像这样:
#![allow(unused)] fn main() { // 注意:'a: { 和 &'b x 不是合法的语法 'a: { let x: i32 = 0; 'b: { // 生命周期是'b,因为这就足够了 let y: &'b i32 = &'b x; 'c: { // 'c也一样 let z: &'c &'b i32 = &'c y; } } } }
哇!这样的写法……太可怕了。我们先停下来感谢Rust把这一切都简化掉了。
将引用传递到作用域以外会导致生命周期扩大:
#![allow(unused)] fn main() { let x = 0; let z; let y = &x; z = y; }
#![allow(unused)] fn main() { 'a: { let x: i32 = 0; 'b: { let z: &'b i32; 'c: { // 必须使用'b,因为引用被传递到了'b的作用域 let y: &'b i32 = &'b x; z = y; } } } }
示例:引用超出被引用内容生命周期
好了,让我们再看一遍曾经举过的一个例子:
#![allow(unused)] fn main() { fn as_str(data: &u32) -> &str { let s = format!("{}", data); &s } }
去掉语法糖:
#![allow(unused)] fn main() { fn as_str<'a>(data: &'a u32) -> &'a str { 'b: { let s = format!("{}", data); return &'a s; } } }
函数as_str
的签名里接受了一个带有一些生命周期的u32类型的引用,并且保证会返回一个生命周期一样长的str类型的引用。从这个签名我们就已经可以看出问题了。它表示我们必须到那个u32引用的作用域,或者比它还要早的作用域里去找一个str。这就有点不合理了。
接下来我们生成一个字符串s
,然后返回它的引用。我们的函数要求这个引用的有效期不能小于'a
,那是我们给引用指定的生命周期。不幸的是,s
是在作用域'b
里面定义的。除非'b
包含'a
这个函数才可能是正确的——而这显然不可能,因为'a
必须包含它所调用的函数。这样我们创建了一个生命周期超出被引用内容的引用,这明显违背了之前提到的引用的第一条规则。编译器十分感动然后拒绝了我们。
我们扩展一下这个例子,一边看得更清楚:
fn as_str<'a>(data: &'a u32) -> &'a str { 'b: { let s = format!("{}", data); return &'a s; } } fn main() { 'c: { let x: u32 = 0; 'd: { // 这里引入了一个匿名作用域,因为借用不需要在整个x的作用域内生效 // as_str的返回值必须引用一个在函数调用前就存在的str // 显然事实不是这样的。 println!("{}", as_str::<'d>(&'d x)); } } }
完蛋了!
当然,这个函数的正确写法应该是这样的。
#![allow(unused)] fn main() { fn to_string(data: &u32) -> String { format!("{}", data) } }
我们必须创建一个值然后连同它的所有权一起返回。除非一个字符串是&'a u32
的成员,我们才能返回&'a str
,显然事情并不是这样的。
(其实我们也可以返回一个字符串的字面量,它是一个全局的变量,可以认为是处于栈的底部。尽管这样有些限制了我们的实现。)
示例:存在可变引用的别名
在看另一个老的例子:
#![allow(unused)] fn main() { let mut data = vec![1, 2,3]; let x = &data[0]; data.push(4); println!("{}", x); }
#![allow(unused)] fn main() { 'a: { let mut data: Vec<i32> = vec![1, 2, 3]; 'b: { // 对于这个借用来说,'b已经足够大了 // (借用只需要在println!中生效即可) let x: &'b i32 = Index::index::<'b>(&'b data, 0); 'c: { // 引入一个临时作用域,因为&mut不需要存在更长时间 Vec::push(&'c mut data, e); } println!("{}", x); } } }
这里的问题更加微妙也更有趣。我们希望Rust出于如下的原因拒绝编译这段代码:我们有一个有效的指向data
的内部数据的引用x
,而同时又创建了一个data
的可变引用用于执行push
。也就是说出现了可变引用的别名,这违背了引用的第二条规则。
但是Rust其实并非因为这个原因判断这段代码有问题。Rust不知道x
是data
的子内容的引用,它其实完全不知道Vec
的内部是什么样子的。它只知道x
必须在'b
范围内有效,这样才能打印其中的内容。函数Index::index
的签名因此要求传递的data
的引用也必须在'b
的范围内有效。当我们调用push
的时候,Rust发现我们要创建一个&'c mut data
。它知道'c
是包含在'b
以内的,因为&'b data
还存活着,所以它拒绝了这段程序。
我们看到了生命周期系统要比引用的保护措施更加简单粗暴。大多数情况下这也没什么,它让我们不用没完没了地向编译器解释我们的程序。但是这也意味着许多语义上正确的程序会被编译器拒绝,因为生命周期的规则太死板了。
生命周期所覆盖的区域
从创建之处到最后一次使用,生命周期(有时称为借用)是有效的。借来的东西只需要借用存活就可以。这看起来很简单,但几乎没有什么微妙之处。
以下代码段将可以编译,因为在打印x
之后不再需要它了,所以它是悬垂的还是别名的都没关系(即使变量x
技术上存在到作用域的最后)。
#![allow(unused)] fn main() { let mut data = vec![1, 2, 3]; let x = &data[0]; println!("{}", x); // 此处可行,x 不再需要。 data.push(4); }
但是,如果该值具有析构函数,则该析构函数将在作用域的末尾运行。运行析构函数被认为是一种使用──显然是最后一次。因此,这将不能编译。
#![allow(unused)] fn main() { #[derive(Debug)] struct X<'a>(&'a i32); impl Drop for X<'_> { fn drop(&mut self) {} } let mut data = vec![1, 2, 3]; let x = X(&data[0]); println!("{:?}", x); data.push(4); // 在这里,析构函数已运行,因此将无法编译。 }
一种使编译器确信x
不再有效的方法是在data.push(4)
之前使用 drop(x)
。
此外,借用可能有多种可能的最后使用,例如在条件的每个分支中。
#![allow(unused)] fn main() { fn some_condition() -> bool { true } let mut data = vec![1, 2, 3]; let x = &data[0]; if some_condition() { println!("{}", x); // 这是该分支中`x`的最后使用 data.push(4); // 所以这里可行 } else { // 这里没有使用`x`,因此有效的最后一个使用是在示例顶部创建x时。 data.push(5); } }
生命周期可能会有停顿。或者,你可能会认为它是两个不同的借用刚好与同一个局部变量绑定在一起。这通常在循环时发生(在循环结束时写入变量的新值,并在下一次迭代的顶部最后一次使用该变量)。
#![allow(unused)] fn main() { let mut data = vec![1, 2, 3]; // 此 mut 允许我们更改应用的指向 let mut x = &data[0]; println!("{}", x); // 借用的最后一次使用 data.push(4); x = &data[3]; // 我们在这里开始新的借用 println!("{}", x); }
从历史上看,Rust 一直保持借用直到作用域结束,因此这些示例可能无法使用较旧的编译器进行编译。另外,在某些极端情况下,Rust 无法正确缩短借用的有效部分,即使看起来像样也无法编译。这些将随着时间的推移而解决。