生命周期
前面章节中讲到引用与借用的时候,里边有一个细节没有讲到,就是生命周期的概念,rust 的每个引用都有自己的生命周期,生命周期最主要的目标在于避免悬垂引用:
认识生命周期
变量会在自身所在的生命周期内有效,当作用域结束后,会将其中的变量销毁,变量的引用也将不再可用:
{
let r;
{ // 开始一个新的作用域
let x = 5; // x被创建了
r = &x; // 报错,x的生命周期不够长
} // 因为x在这里销毁
println!("r: {}", r);
}
// 因为r是x的引用,而x销毁的时候,x的引用也自然不可用了
// 所有r也会变成悬垂引用,这在编译过程是不通过的
复制代码
rust 并不是任何时候都可以识别生命周期,比如将引用作为函数返回值时,编译器不能识别返回的引用是来自于哪个参数:
fn longest(x: &str, y: &str) -> &str { // 报错,返回值需要指定生命周期
if x.len() > y.len() {
x
} else {
y
}
}
复制代码
有同学可能觉得是因为有判断逻辑的原因,所以编译器不确定会返回哪个值,下面可以去掉判断试一下:
fn longest(x: &str, y: &str) -> &str { // 报错,返回值需要指定生命周期
x
}
复制代码
还是报错了,说明错误原因的不在于函数体,而是在于开发者的传参,参数需要与返回值产生联系,如果没有联系,那么 rust 将无法在编译阶段得知函数执行后的返回值会在什么时候销毁,标注生命周期以后,就可以限制函数的调用者的使用方式。
生命周期注解语法
生命周期参数名称必须以单引号(')开头,其名称通常全是小写,类似于泛型,其名称非常短。'a 是惯用使用的名称。生命周期参数注解位于引用符号 &之后,并有一个空格来将引用类型与生命周期注解分隔开:
&i32 // 普通引用
&'a i32 // 带有显式生命周期的引用
&'a mut i32 // 带有显式生命周期的可变引用
复制代码
下面来尝试对 longest 函数进行标注生命周期:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
// 对x和y都标记了生命周期为'a,返回值的生命周期也是'a
// 意味着longest的返回值的生命周期截止是'a的销毁时间,
// 而'a代表了运行时x、y中较短的那个。
if x.len() > y.len() {
x
} else {
y
}
}
// 可以正常编译通过了
println!("{}", longest("abc", "a")); // abc
复制代码
通过传递拥有不同具体生命周期的引用来限制 longest 函数的使用:
let string1 = String::from("abcd");
{
let string2 = String::from("xyz");
let result = longest(string1.as_str(), string2.as_str());
println!("最长的字符串是:{}", result); // 最长的字符串是:abcd
}
// 上边由于传参符合函数对于生命周期的要求,
// string2是x、y参数中生命周期最短的那个,
// 根据函数签名推断出,返回值result与string2的生命周期的长度都是'a,
// 而在作用于结束后,外部没有对string2悬垂引用,所以编译通过了
复制代码
知道了 result 的生命周期和 string2 相同,也就能推断出悬垂引用:
fn main() {
let string1 = String::from("abcd");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str()); // 报错, string2.as_str()的生命周期不够长
}
println!("最长的字符串是:{}", result);
}
// 再重复一下,
// 由于我们对函数参数和返回值的生命周期标注为x、y中较短的那一个,
// 上面string2的生命周期最短,也就是说result和string2的生命周期是相同的
// 当string2所在的作用域被销毁后
// result仍被外部作用域引用,也就产生了悬垂引用,所以报错了。
复制代码
如果都函数签名中生命周期没有争议,也可以编译通过:
fn longest<'a>(x: &'a str, y: &str) -> &'a str {
x
}
// 因为编译器能够很明确知道返回值的引用来自于x,
// 因为y没有被标注生命周期。
复制代码
那如果返回值是函数体中创建的引用呢:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { // 对生命周期做了标记
let s = String::new(); // 返回值是函数体中创建的引用
&s // 报错,不能返回对局部变量s的引用,返回值是对当前函数的引用
}
// s在函数执行完成后被销毁,&s变成了悬垂引用,
// 整个过程与参数、返回值的生命周期标注并没有发生关联,所以报错。
复制代码
结构体定义中的生命周期注解
如果结构体中的字段是引用类型,那么同样需要标注生命周期:
struct ImportantExcerpt<'a> { // 在结构体名称后面声明
part: &'a str, // 表示part的生命周期至少要比结构体长,否则就产生的悬垂引用
}
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 };
// 这里first_sentence和i都在main函数结束后被销毁
// first_sentence早于i创建,所以能够正常编译通过
}
复制代码
生命周期省略(Lifetime Elision)
生命周期都符合以下三条规则,编译器利用这三条规则可以让开发者省略部分生命周期的标注,提高开发效率:
看一个可以省略生命周期的例子:
// 符合规则一和二
fn first_word(s: &str) -> &str {}
// 编译器自动生成
fn first_word<'a>(s: &'a str) -> &'a str {}
复制代码
这个例子则需要开发者手动标注:
fn longest(x: &str, y: &str) -> &str {}
// 不符合规则二和三,需要开发者手动标注
复制代码
为包含引用类型字段的结构体,为其实现方法的时候,也需要标注生命周期:
impl<'a> ImportantExcerpt<'a> {
// impl 之后和类型名称之后的生命周期参数是必要的,
// 不过因为第一条生命周期规则我们并不必须标注self引用的生命周期。
fn level(&self) -> i32 {
1
}
}
复制代码
下面 announce_and_return_part 函数参数中包含 self 引用,结合第三条规则,生命周期是可以省略的:
impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part(&self, announcement: &str) -> &str {
self.part
}
}
复制代码
静态生命周期
静态生命周期能够存活于整个程序期间。所有的字面值都拥有 'static 生命周期:
let s = "I have a static lifetime.";
// 等同于
let s: &'static str = "I have a static lifetime.";
复制代码
作为函数返回值:
// 上面的标注方式
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
&"abc"
}
// 或者直接标识返回值是静态的
fn longest<'a>(x: &str, y: &str) -> &'static str {
&"abc"
}
复制代码
编译通过了,是因为字面量的字符串"abc"是被 rust硬编码到编译结果中,这是不会被销毁的,事实上所有的字面量都是可以的:
fn longest<'a>(x: &'a str, y: &'a str) -> (&'a str, &'a i32) {
(&"abc", &123)
}
println!("{:?}", longest("a", "b"));// ("abc", 123)
// 上边返回一个元组,包含两个字面量,编译是正常的。
复制代码
同时使用泛型参数、trait 约束与生命周期
因为生命周期也是泛型的一种,所以生命周期参数'a 和泛型参数 T 都被放置到了函数名后的尖括号列表中:
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
}
}
longest_with_an_announcement("a", "b", 1); // Announcement! 1
复制代码
生命周期理解起来还是不容易的,所以下一节打算再深度剖析一下为什么要设计生命周期
评论