生命周期
概念
Rust 的又一个重要特性之一。Rust 里每个引用都有自己的生命周期,生命周期就是让引用保持有效的作用域,大多数时候生命周期是隐式的、可以被推断出来的。当引用的生命周期可能以不同的方式互相关联时,就需要手动标注生命周期。生命周期的主要目标就是避免悬垂引用。
下面的代码报错借用的值没有足够长的生命周期,这是因为代码执行完里面的花括号作用域后,x 已经被释放了,x 的引用不能赋值给 r。这里面其实用到了借用检查器:用来比较作用域来判断所有的借用是否合法
fn main() {
let r;
{
let x = 32;
//报错:borrowed value does not live long enough
r = &x;
}
println!("{}", r);
}
下面再来看个生命周期的例子
fn main() {
{
// 'a生命周期,'a生命周期覆盖了'b生命周期。'b的生命周期比较短,所以不能把值借用给'a生命周期上的变量
let r;
{
// `b生命周期
let x = 32;
// 报错:borrowed value does not live long enough
r = &x;
}
println!("{}", r);
}
}
fn main() {
// 像这样是没问题的,因为 x 的生命周期包含了 r 的生命周期
let x = 32;
let r = &x;
println!("{}", r);
}
生命周期泛型例子
看个生命周期的相关例子,产生错误的原因是编译器不知道传入参数 str1 或者 str2 的生命周期,借用检查器也不知道返回类型的生命周期是跟哪个有关系。这里看不懂就先暂时看着,后面会解释
fn main() {
let s1 = String::from("hello world");
let s2 = "I am Wang Dachui";
let longest = get_longest(&s1.as_str(), &s2);
println!("{}", longest)
}
// 报错:返回类型包括一个借用的值,但是签名不知道它是从 str1 借用还是 str2 借用的,可以加上生命周期参数
// help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `str1` or `str2`
// help: consider introducing a named lifetime parameter
// 类似于↓这样
// fn get_longest<'a>(str1: &'a str, str2: &'a str) -> &'a str {
fn get_longest(str1: &str, str2: &str) -> &str {
if (str1.len() > str2.len()) {
str1
} else {
str2
}
}
fn main() {
let s1 = String::from("hello world");
let s2 = "I am Wang Dachui";
let longest = get_longest(&s1.as_str(), &s2);
println!("{}", longest)
}
// 标注生命周期泛型,然后像这样写程序就能正常运行了。
// 这里的意思是:get_longest函数有一个'a的生命周期标注,它有两个参数 str1 和 str2 都是字符串切片类型引用,
// 这两个参数的存活时间必须不能短于 'a,而对于返回值,也不能短于 'a 这个生命周期
// 其实就是为了告诉编译器,生命周期不一样时不能给你编译通过
// 这里的 'a 其实能获得的生命周期就是 str1 和 str2 中比较短的那一个,取交集
fn get_longest<'a>(str1: &'a str, str2: &'a str) -> &'a str {
if (str1.len() > str2.len()) {
str1
} else {
str2
}
}
生命周期标注语法
- 生命周期的标注不会改变引用的生命周期长度
- 当指定了泛型生命周期参数,函数可以接收带有任何生命周期的引用
- 生命周期的标注:描述了多个引用的生命周期之间的关系,但不影响生命周期
语法
生命周期参数名:以'开头,通常以全小写开头且很短,很多人喜欢用 'a 做生命周期标注的名称
生命周期标注位置:在引用的 & 符号后面,使用空格将标注和引用类型分开
例子:
&i32 // 一个引用
&'a i32 // 带有显式生命周期的引用
&'a mut i32 // 带有显式生命周期的可变引用
单个生命周期的标注没有什么意义
函数签名中的生命周期标注
泛型生命周期参数声明在:函数名和参数列表之间的<>里
生命周期'a 的实际生命周期就是 str1 和 str2 中比较小的那个。
fn main() {
let s1 = String::from("hello world");
let longest;
{
let s2 = String::from("I am Wang Dachui");
// 这里 s2 发生报错: borrowed value does not live long enough
// 借用的值没有总够长的生命周期
longest = get_longest(&s1.as_str(), &s2.as_str());
}
// s2 在离开作用域后已经被 drop 函数回收,所以不能够被借用
println!("{}", longest)
}
fn get_longest<'a>(str1: &'a str, str2: &'a str) -> &'a str {
if (str1.len() > str2.len()) {
str1
} else {
str2
}
}
深入理解生命周期
指定生命周期参数的方式依赖于函数所做的事情
rust// 这个函数返回了,所以生命周期只和 str1 有关,跟 str2 无关,就可以能把 str2 的生命周期标注给去了 fn get_longest<'a>(str1: &'a str, str2: &str) -> &'a str { str1 }
从函数返回引用时,返回类型的生命周期参数需要与其中一个参数的生命周期匹配
如果返回的引用没有指向任何参数,那么它只能引用函数内创建的值:这就是垂悬引用,这个值在函数结束的时候就离开了作用域
rustfn test<'a>(str1: &'a str, str2: &'a str) -> &'a str { let res = String::from("hello"); // 报错:returns a value referencing data owned by the current function // 返回了一个当前函数拥有的相关数据 res.as_str() } // 如果想返回内部的值给外部使用,那么就不返回引用 fn test<'a>(str1: &'a str, str2: &'a str) -> String { let res = String::from("hello"); res }
Struct 定义中的生命周期标注
Struct 内可以包括:
- 基本数据类型
- 引用:需要在每个引用上添加生命周期标注
struct ImportantExcerpt<'a> {
// part 引用存活的时间必须要比实例的存活时间要长
// 只要实例存在就会一直对 part 有引用
part: &'a str,
}
fn main() {
let novel = String::from("很久很久以前,有一个...");
let first_sentence = novel.split(",").next().expect("找不到,");
let i = ImportantExcerpt {
// 这里 part 生命周期其实就是从 first_sentence 创建到离开 main 作用域
// 它的生命周期比 i 的生命周期要长
part: first_sentence,
};
}
生命周期的省略概念
在 Rust 引用分析中所编入的模式称为生命周期的省略规则,也就是一些可以预测的生命周期已经被写到编译器中,我们就不需要手动去写生命周期标注了。
输入、输出生命周期的概念
生命周期在函数/方法的参数中,那么就叫做输入生命周期。
输入出现在函数/方法的返回值中,就叫做输出生命周期。
生命周期省略的三个规则
编译器使用 3 个规则在没有显式标注生命周期的情况下,来确定引用的生命周期,适用于 fn 定义和 impl 块。如果编译器应用完这三个规则之后,仍然无法确定生命周期的引用就会报错。规则 1 应用于输入生命周期,规则 2、3 应用于输出生命周期。
- 规则 1:每个引用类型的参数都有自己的生命周期
- 规则 2:如果只有 1 个输入生命周期参数,那么该生命周期被赋给所有的输出生命周期参数
- 规则 3:如果有多个输入生命周期参数,但其中一个是&self 或&mut self(方法才有 self),那么 self 的生命周期会被赋给所有的输出生命周期参数
例子 1:
假如我们是编译器,那么应用这三条规则后会发生什么
// 我们写的代码↓
fn example(s: &str) -> &str {}
// 应用第一条规则后
fn example<'a>(s: &'a str) -> &str {}
// 应用第二条规则后
fn example<'a>(s: &'a str) -> &'a str {}
例子 2:
// 我们写的代码↓
fn example2(x: &str, y: &str) -> &str {}
// 应用第一条规则后
fn example2<'a, 'b>(x: &'a str, y: &'b str) -> &str {}
// 第二条规则不适用,因为函数有两个参数
// 第三条规则也不适用,因为这不是一个方法
// 应用完三条规则后,编译器也不能确定生命周期,然后就会报错
方法定义中的生命周期标注
第三条规则只适用于方法
在哪声明和使用生命周期,依赖于:生命周期参数是否和字段、方法的参数或返回值有关
struct 字段的生命周期名:
- 在 impl 后声明
- 在 struct 名后使用
- 这些生命周期是 struct 类型的一部分
impl 块内的方法签名中:
- 引用必须绑定于 struct 字段引用的生命周期,或者引用是独立的也可以
- 生命周期省略规则经常使得方法中的生命周期标注不是必须的
下面的代码编译没问题,根据第三条规则,self 的生命周期会被赋给所有的输出生命周期参数
struct ImportantExcerpt<'a> {
part: &'a str,
}
impl<'a> ImportantExcerpt<'a> {
fn example(&self) -> i32 {
3
}
fn example2(&self, str1: &str, str2: &str) -> &str {
self.part
}
}
fn main() {}
静态生命周期'static
’static 是一个特殊的生命周期:表示整个程序的持续时间,比如所有的字符串字面值都拥有'static 生命周期。
比如:
// 它存储在二进制文件中,所以它总是可用的
let s: &'static str = "I have a dream";
为引用指定'static 生命周期前要思考:是否需要引用在程序整个生命周期中存活?
泛型参数类型、Trait Bound、生命周期综合例子
use std::fmt::Display;
fn longest_mark<'a, T>(x: &'a str, y: &'a str, mark: T) -> &'a str
where
T: Display,
{
println!("mark is {}", mark);
if (x.len() > y.len()) {
x
} else {
y
}
}
fn main() {}
总结
- 所有使用了引用的函数,都需要生命周期的标注。
- 所有引用类型的参数都有独立的生命周期 'a 、'b 等。
- 如果只有一个引用型输入,它的生命周期会赋给所有输出。
- 如果有多个引用类型的参数,其中一个是 self,那么它的生命周期会赋给所有输出。
- 使用数据结构时,数据结构自身的生命周期,需要小于等于其内部字段的所有引用的生命周期。