生命周期
源:lifetimes.md Commit: 0e6c680ebd72f1860e46b2bd40e2a387ad8084ad
Rust在整个生命周期里强制执行生命周期的规则。生命周期说白了就是作用域的名字。每一个引用以及包含引用的数据结构,都要有一个生命周期来指定它保持有效的作用域。
在函数体内,Rust通常不需要你显式地给生命周期起名字。这是因为在本地上下文里,一般没有必要关注生命周期。Rust知道程序的全部信息,从而可以完美地执行各种操作。它可能会引入许多匿名或者临时的作用域让程序顺利执行。
但是如果你要跨出函数的边界,就需要关心生命周期了。生命周期用这样的符号表示:'a
,'static
。为了更清晰地了解生命周期,我们假设我们可以为生命周期打标签,去掉本章所有例子的语法糖。
最开始,我们的示例代码对作用域和生命周期使用了很激进的语法糖特性——甜得像玉米糖浆一样,因为把所有的东西都显式地写出来实在很讨厌。所有的Rust代码都采用比较激进的理论以省略“显而易见”的东西。
一个特别有意思的语法糖是,每一个let
表达式都隐式引入了一个作用域。大多数情况下,这一点并不重要。但是当变量之间互相引用的时候,这就很重要了。举个简单的例子,我们彻底去掉下面这段代码的语法糖:
let x = 0;
let y = &x;
let z= &y;
借用检查器通常会尽可能减少生命周期的范围,所以去掉语法糖后的代码大概像这样:
// 注意:'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把这一切都简化掉了。
将引用传递到作用域以外会导致生命周期扩大:
let x = 0;
let z;
let y = &x;
z = y;
'a: {
let x: i32 = 0;
'b: {
let z: &'b i32;
'c: {
// 必须使用'b,因为引用被传递到了'b的作用域
let y: &'b i32 = &'b x;
z = y;
}
}
}
示例:引用超出被引用内容生命周期
好了,让我们再看一遍曾经举过的一个例子:
fn as_str(data: &u32) -> &str {
let s = format!("{}", data);
&s
}
去掉语法糖:
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));
}
}
}
完蛋了!
当然,这个函数的正确写法应该是这样的。
fn to_string(data: &u32) -> String {
format!("{}", data)
}
我们必须创建一个值然后连同它的所有权一起返回。除非一个字符串是&'a u32
的成员,我们才能返回&'a str
,显然事情并不是这样的。
(其实我们也可以返回一个字符串的字面量,它是一个全局的变量,可以认为是处于栈的底部。尽管这样极大限制了函数的使用场合。)
示例:存在可变引用的别名
在看另一个老的例子:
let mut data = vec![1, 2,3];
let x = &data[0];
data.push(4);
println!("{}", x);
'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
还存活着,所以它拒绝了这段程序。
我们看到了生命周期系统要比引用的保护措施更加简单粗暴。大多数情况下这也没什么,它让我们不用没完没了地向编译器解释我们的程序。但是这也意味着许多语义上正确的程序会被编译器拒绝,因为生命周期的规则太死板了。