rust生命周期
生命周期是rust中用来规定引用的有效作用域。在大多数时候,无需手动声明,因为编译器能够自动推导。当编译器无法自动推导出生命周期的时候,就需要我们手动标明生命周期。生命周期主要是为了避免悬垂引用。
借用检查
rust的编译器会使用借用检查器来检查我们程序的借用正确性。例如:
代码语言:javascript复制#![allow(unused)]
fn main() {
{
let r;
{
let x = 5;
r = &x;
}
println!("r: {}", r);
}
}
在编译期,Rust 会比较两个变量的生命周期,结果发现 r 明明拥有生命周期 'a,但是却引用了一个小得多的生命周期 'b,在这种情况下,编译器会认为我们的程序存在风险,因此拒绝运行。
函数中的生命周期
代码语言:javascript复制#![allow(unused)]
fn main() {
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
}
执行这段代码,rust编译器会报错,它给出的help信息如下:
代码语言:javascript复制help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`
意思是函数返回类型是一个借用值,但是无法从函数的签名中得知返回值是从x还是y借用的。并且给出了相应的修复代码。
代码语言:javascript复制4 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str
|
按照这个提示,我们更改函数声明。就会发现可以顺利通过编译。因此,像这样的函数,我们无法判断它是返回x还是y,那么只好手动进行生命周期声明。上面的提示就是手动声明声明周期的语法。
手动声明生命周期
需要注意的是,标记的生命周期只是为了取悦编译器,让编译器不要难为我们,它不会改变任何引用的实际作用域。
生命周期的语法是以’开头,名称往往是一个单独的小写字母。大多数人用’a来作为生命周期的名称。如果是引用类型的参数,生命周期会位于&之后,并用空格来将生命周期和参数分隔开。函数签名中的生命周期标注和泛型一样,需要在提前声明生命周期。例如我们刚才修改过的函数签名
代码语言:javascript复制fn longest<'a>(x: &'a str, y: &'a str) -> &'a str
该函数签名表明对于某些生命周期 'a,函数的两个参数都至少跟 'a 活得一样久,同时函数的返回引用也至少跟 'a 活得一样久。实际上,这意味着返回值的生命周期与参数生命周期中的较小值一致:虽然两个参数的生命周期都是标注了 'a,但是实际上这两个参数的真实生命周期可能是不一样的(生命周期 'a 不代表生命周期等于 'a,而是大于等于 'a)。例如:
代码语言:javascript复制fn main() {
let string1 = String::from("long string is long");
{
let string2 = String::from("xyz");
let result = longest(string1.as_str(), string2.as_str());
println!("The longest string is {}", result);
}
}
result 的生命周期等于参数中生命周期最小的,因此要等于 string2 的生命周期,也就是说,result 要活得和 string2 一样久。如过我们将上面的代码改变为如下所示。
代码语言:javascript复制fn main() {
let string1 = String::from("long string is long");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
}
println!("The longest string is {}", result);
}
那么将会导致错误,因为编译器知道string2活不到最后一行打印。而string1可以活到打印,但是编译器并不知道longest返回的是谁。 函数的返回值如果是一个引用类型,那么它的生命周期只会来源于:
- 函数参数的生命周期
- 函数体中某个新建引用的生命周期
若是后者情况,就是典型的悬垂引用场景:
代码语言:javascript复制#![allow(unused)]
fn main() {
fn longest<'a>(x: &str, y: &str) -> &'a str {
let result = String::from("really long string");
result.as_str()
}
}
上面的函数的返回值就和参数 x,y 没有任何关系,而是引用了函数体内创建的字符串,而函数结束的时候会自动释放result的内存,从而导致悬垂指针。这种情况,最好的办法就是返回内部字符串的所有权,然后把字符串的所有权转移给调用者:
代码语言:javascript复制fn longest<'a>(_x: &str, _y: &str) -> String {
String::from("really long string")
}
fn main() {
let s = longest("not", "important");
}
结构体中的生命周期
在结构体中使用引用,只要为结构体中的每一个引用标注上生命周期即可。
代码语言:javascript复制struct ImportantExcerpt<'a> {
part: &'a str,
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().expect("Could not find a '.'");
let i = ImportantExcerpt {
part: first_sentence,
};
}
part引用的first_sentence来自于novel,它的生命周期是main函数,因此这段代码可以正常工作。 ImportantExcerpt 结构体中有一个引用类型的字段 part,因此需要为它标注上生命周期。结构体的生命周期标注语法跟泛型参数语法很像,需要对生命周期参数进行声明 <'a>。该生命周期标注说明,结构体 ImportantExcerpt 所引用的字符串 str 必须比该结构体活得更久。
生命周期消除
编译器为了简化用户的使用,运用了生命周期消除大法。例如:
代码语言:javascript复制fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
对于 first_word 函数,它的返回值是一个引用类型,那么该引用只有两种情况:
- 从参数获取
- 从函数体内部新创建的变量获取
如果是后者,就会出现悬垂引用,最终被编译器拒绝,因此只剩一种情况:返回值的引用是获取自参数,这就意味着参数和返回值的生命周期是一样的。道理很简单,我们能看出来,编译器自然也能看出来,因此,就算我们不标注生命周期,也不会产生歧义。
只不过,消除规则不是万能的,若编译器不能确定某件事是正确时,会直接判为不正确,那么你还是需要手动标注生命周期 函数或者方法中,参数的生命周期被称为 输入生命周期,返回值的生命周期被称为 输出生命周期。
三条消除原则
- 每一个引用参数都会获得独自的生命周期 例如一个引用参数的函数就有一个生命周期标注: fn foo<'a>(x: &'a i32),两个引用参数的有两个生命周期标注:fn foo<'a, 'b>(x: &'a i32, y: &'b i32), 依此类推。
- 若只有一个输入生命周期(函数参数中只有一个引用类型),那么该生命周期会被赋给所有的输出生命周期,也就是所有返回值的生命周期都等于该输入生命周期 例如函数 fn foo(x: &i32) -> &i32,x 参数的生命周期会被自动赋给返回值 &i32,因此该函数等同于 fn foo<'a>(x: &'a i32) -> &'a i32
- 若存在多个输入生命周期,且其中一个是 &self 或 &mut self,则 &self 的生命周期被赋给所有的输出生命周期。 拥有 &self 形式的参数,说明该函数是一个 方法,该规则让方法的使用便利度大幅提升。
让我们假装自己是编译器,然后看下以下的函数该如何应用这些规则:
例子1
代码语言:javascript复制fn first_word(s: &str) -> &str // 实际项目中的手写代码
首先,我们手写的代码如上所示时,编译器会先应用第一条规则,为每个参数标注一个生命周期:
代码语言:javascript复制fn first_word<'a>(s: &'a str) -> &str // 编译器自动为参数添加生命周期
此时,第二条规则就可以进行应用,因为函数只有一个输入生命周期,因此该生命周期会被赋予所有的输出生命周期:
代码语言:javascript复制fn first_word<'a>(s: &'a str) -> &'a str // 编译器自动为返回值添加生命周期
此时,编译器为函数签名中的所有引用都自动添加了具体的生命周期,因此编译通过,且用户无需手动去标注生命周期,只要按照 fn first_word(s: &str) -> &str
的形式写代码即可。
例子2
代码语言:javascript复制fn longest(x: &str, y: &str) -> &str // 实际项目中的手写代码
首先,编译器会应用第一条规则,为每个参数都标注生命周期:
代码语言:javascript复制fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str
但是此时,第二条规则却无法被使用,因为输入生命周期有两个,第三条规则也不符合,因为它是函数,不是方法,因此没有 &self 参数。在套用所有规则后,编译器依然无法为返回值标注合适的生命周期,因此,编译器就会报错,提示我们需要手动标注生命周期。
例子3
代码语言:javascript复制impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {}", announcement);
self.part
}
}
首先,编译器应用第一规则,给予每个输入参数一个生命周期。
代码语言:javascript复制impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part<'b>(&'a self, announcement: &'b str) -> &str {
println!("Attention please: {}", announcement);
self.part
}
}
需要注意的是,编译器不知道 announcement 的生命周期到底多长,因此它无法简单的给予它生命周期 'a,而是重新声明了一个全新的生命周期 'b。接着,编译器应用第三规则,将 &self 的生命周期赋给返回值 &str
代码语言:javascript复制impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part<'b>(&'a self, announcement: &'b str) -> &'a str {
println!("Attention please: {}", announcement);
self.part
}
}
尽管我们没有给方法标注生命周期,但是在第一和第三规则的配合下,编译器依然完美的为我们亮起了绿灯。
生命周期约束
我们来看下面这个例子。将返回值的生命周期声明为’b,但是实际返回的是生命周期为’a的self.part。
代码语言:javascript复制impl<'a: 'b, 'b> ImportantExcerpt<'a> {
fn announce_and_return_part(&'a self, announcement: &'b str) -> &'b str {
println!("Attention please: {}", announcement);
self.part
}
}
- 'a: 'b,是生命周期约束语法,跟泛型约束非常相似,用于说明 'a 必须比 'b 活得久
- 可以把 'a 和 'b 都在同一个地方声明(如上),或者分开声明但通过 where 'a: 'b 约束生命周期关系,如下:
impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part<'b>(&'a self, announcement: &'b str) -> &'b str
where
'a: 'b,
{
println!("Attention please: {}", announcement);
self.part
}
}
加上这个约束,告诉编译器’a活的比’b更久,引用’a不会产生悬垂指针(无效引用)。
静态生命周期
rust中有一个非常特殊的生命周期,那就是’static,拥有该生命周期的引用可以活的和整个程序一样久。实际上字符串字面值就拥有’static生命周期,它被硬编码进rust的二进制文件中。'static生命周期非常强大,随意使用它相当于放弃了生命周期检查。遇到因为生命周期导致的编译不通过问题,首先想的应该是:是否是我们试图创建一个悬垂引用,或者是试图匹配不一致的生命周期,而不是简单粗暴的用 'static 来解决问题。除非实在遇到解决不了的生命周期标注问题,可以尝试’static生命周期。例如:
代码语言:javascript复制fn t() -> &'static str{
"qwert"
}
注意,使用’static生命周期的时候,不需要提前声明。
一个复杂例子: 泛型,特征约束以及生命周期
代码语言:javascript复制use std::fmt::Display;
fn longest_with_an_announcement<'a, T>(
x: &'a str,
y: &'a str,
ann: T,
) -> &'a str
where
T: Display,
{
println!("Announcement! {}", ann);
if x.len() > y.len() {
x
} else {
y
}
}
例子中,包含了生命周期’a,泛型T以及对T的约束Display(因为我们需要打印ann)。