本文已参与「新人创作礼」活动, 一起开启掘金创作之路。
生命周期(lifetime)
- 生命周期的主要目的是:避免悬垂引用。
- 规则: 任何引用本身都不能比它所引用的对象存活地更久。
- Rust 的每个引用都有自己的生命周期,使得引用保持有效的作用域。
- 大多数情况:生命周期是隐式的、可被推断的。
- 当引用的生命周期可能以不同的方式互相关联时:就要手动标注生命周期。
实现手段
- Rust 编译器有一个 借用检查器(borrow checker),它比较作用域来判断所有的借用是否有效。
- 使用泛型生命周期参数来定义引用间的关系,以便借用检查器可以进行分析。
编译器通过借用检查器分析相关变量的生命周期来实现这一点。如果引用的生命周期小于被引用的 生命周期,编译成功,否则编译失败。
生命周期的标注语法
- 生命周期参数命:
- 以
'开头 - 通常全小写且非常短
- 很多人使用
'a
- 以
- 生命周期参数标注位置:
- 在引用的
&符号后 - 使用空格将标注和引用类型分开
- 在引用的
- 在引用的情况下才会出现生命周期标注 。
- 最重要的是,生命周期标注会传播借用状态!它是描述多个引用生命周期之间的相互关系,不会改变引用的生命周期长度,也因此单个的生命周期标注没有多少意义。
- 当指定了泛型生命周期参数,函数可以接受带有任何生命周期的引用。
-
函数签名中生命周期参数的标注和约定:
- 在函数签名中泛型生命周期参数声明在:函数名和参数列表间的
<>中。 - 泛型生命周期
'a的具体生命周期是参数列表中x和y的所传入的引用生命周期中较小的那一个。 - 当从函数返回一个引用,返回值的生命周期参数需要与一个参数的生命周期参数相匹配。
- 如果返回的引用没有指向任何一个参数,那么唯一的可能就是它指向一个函数内部创建的值,它将会是一个悬垂引用,因为它将会在函数结束时离开作用域。
- 在函数签名中泛型生命周期参数声明在:函数名和参数列表间的
-
结构体定义中的生命周期参数的标注:
- 定义结构体字段的数据类型时:有自持有所有权的数据类型,也可以有不持有所有权的引用类型。技术上,结构体可能不持有任何数据。
- 在定义结构体时泛型生命周期参数声明在:结构体名称后面的
<>中。
-
方法定义中的生命周期参数的标注
- 在 struct 上使用生命周期实现方法,语法和泛型参数的语法一样
- 在哪声明和使用生命周期参数,依赖于:
- 生命周期参数是否和字段、方法的参数或返回值有关
- struct 字段的生命周期名:
- 在 impl 后声明
- 在struct 名后使用
- 这些生命周期是struct类型的一部分
- impl 块内的方法签名中:
- 引用必须绑定于 struct 字段引用的生命周期,或者引用是独立的也可以
- 生命周期省略规则经常使得方法中的生命周期标注不是必须的
-
静态生命周期
- 使用
'static来标注。
- 使用
-
在同一函数中指定泛型类型参数、trait bounds 和生命周期的语法
- 生命周期参数
'a和泛型类型参数T都位于函数名后的同一<>列表中。
- 生命周期参数
生命周期省略(Lifetime Elision)
-
定义和约定:
- 在Rust 引用分析中所编入的模式被称为 生命周期省略规则(lifetime elision rules)。
- 这些规则无需开发者来遵守。
- 它们是一系列特定的场景,由编译器来考虑。
- 如果代码符合这些场景,就无需显示标注生命周期。
-
两个概念:
- 在函数或方法的参数中称为:输入生命周期。
- 在函数和方法的返回值中称为:输出生命周期。
规则:
- 第一条规则是每一个是引用的参数都有它自己的生命周期参数。换句话说就是,有一个引用参数的函数有一个生命周期参数,有两个引用参数的函数就有两个生命周期参数,依此类推。
- 第二条规则是如果只有一个输入生命周期参数,那么它被赋予所有输出生命周期参数。
- 第三条规则是当有多个传入参数引用时,但第一个参数是:
&self/&mut self说明是个对象的方法(method),在这种情况下,输入参数的生命周期注释也被赋值给所有输出引用。例如:fn some_method(&self) -> &str等价于fn some_method<'a>(&'a self) -> &'a str第三条规则使得方法更容易读写,因为只需更少的符号。 编译器使用 3 个规则在没有显式标注生命周期的情况下,来确定引用的生命周期:
- 规则 1 应用于输入生命周期。
- 规则 2、3 应用于输出生命周期。
- 如果编译器应用完 3 个规则之后,仍然有无法确定生命周期的引用 → 报错。
- 这些规则适用于 fn 定义和 impl 块。
被省略的不是生命周期,而是生命周期的标注和扩展的生命周期约束。
- 在 Rust 编译器的早期版本中,不允许省略,并且需要对每个生命周期进行标注。
- 但随着时间的推移,编译器团队观察到生命周期注释的相同模式被重复,因此修改编译器规则,从而推断它们。
- 生命周期标注省略减少了代码中的混乱,未来编译器可能会推断出更多模式的生命周期约束。
理清一些困惑
生命周期如此令人困惑的部分原因是在 Rust 的大部分文章中,生命周期这个词被松散地用于指代三种不同的东西:
- 变量的实际生命周期 -> 每一个初始化了的变量,都有它自己的生命周期,即变量的作用域。例如:
{
let x: Vec<i32> = Vec::new();//---------------------+
{// |
let y = String::from("Why");//---+ | x的生命周期
// | y的生命周期 |
}// <--------------------------------+ |
}// <---------------------------------------------------+
- 生命周期的约束 -> 变量的交互模式而产生的约束,换句话说就是变量之间的相互联系。代码中变量之间的交互模式,对它们的生命周期产生了一些限制。例如,在下面的代码中:
x = &y;这一行就添加了一个约束,即:x的生命周期应该包含在y的生命周期内 ( x ≤ y):
//error:`y` does not live long enough
{
let x: &Vec<i32>;
{
let y = Vec::new();//----+
// | y的生命周期
// |
x = &y;//----------------|--------------+
// | |
}// <------------------------+ | x的生命周期
println!("x's length is:{}", x.len());// |
}// <-------------------------------------------+
在此例中,如果没有添加这个约束,println! 代码块中访问的x是一个未初始化的变量,编译不能通过。而添加这个约束,println! 代码块中x是对y的引用,y的寿命不够长,访问的x是一个无效的内存,因为它在前一行就被销毁了,编译不能通过。
如果去掉里面一层的花括号,则编译通过,输出x's length is: 0,x的值是一个对y的引用,而y的引用指向一个值是空的动态数组,因此长度为0
需要注意的是:约束不会改变实际的生存期 —— 例如,x 的生命周期 实际上仍然会扩展到外部块的末尾 ——生命周期标注只是编译器用来禁止悬空引用的工具。在上面的例子中,实际的生命周期不符合规则约束,x 的生命周期已经超出了 y 的生命周期。因此,这段代码无法编译。
- 生命周期标注 -> 标注符
'a是一个通用的泛型生命周期参数,使得变量可以在不同的方式相互关联时指定特定的作用域。很多时候编译器会(自动)生成所有的生命周期约束。但是随着代码变得越来越复杂,编译器会要求开发者手动添加约束。程序员通过生命周期注释来实现这一点。例如,在下面的代码中,编译器需要知道print_ret()函数返回的引用是借用了 s1 还是 s2,所以编译器要求程序员显式地添加这个约束:
//error:missing lifetime specifier
//this function's return type contains a borrowed value,
//but the signature does not say whether it is borrowed from `s1` or `s2`
fn print_ret(s1: &str, s2: &str) -> &str {
println!("s1 is {}", s1);
s2
}
fn main() {
let some_str: String = "Some string".to_string();
let other_str: String = "Other string".to_string();
let s1 = print_ret(&some_str, &other_str);
}
📒: 思考为什么编译器不能看到输出引用是从 s2 中借来的。
然后,开发者用 'a 标记 s2 和返回的引用,用来告诉编译器,返回值是从 s2 中借来的。
fn print_ret<'a>(s1: &str, s2: &'a str) -> &'a str {
println!("s1 is {}", s1);
s2
}
fn main() {
let some_str: String = "Some string".to_string();
let other_str: String = "Other string".to_string();
let s1 = print_ret(&some_str, &other_str);
}
不过要强调的是,仅仅因为 'a 标记在参数 s2 和返回的引用上,并不意味着 s2 和返回的引用都有完全相同的生命周期。相反,这应该被理解为:带有 'a 标记的返回引用是从具有相同标记的参数中借用来的。 由于 s2 进一步借用了 other_str,生命周期约束是返回的引用不能超过 other_str 的 生命周期。这里满足约束,编译成功:
fn print_ret<'a>(s1: &str, s2: &'a str) -> &'a str {
println!("s1 is {}", s1);
s2
}
fn main() {
let some_str: String = "Some string".to_string();
let other_str: String = "Other string".to_string();//-------------+
let ret = print_ret(&some_str, &other_str);//---+ | other_str的生命周期
// | ret的生命周期 |
}// <-----------------------------------------------+-----------------+
总结
记住,通过用 'a 标记引用,程序员只是在构造一些约束;然后,编译器的工作就是为 'a 找到满足其约束的具体生命周期。
变量的生命周期必须满足编译器和开发者对它们施加的某些约束,然后编译器才能确保代码是合理的。如果没有生命周期这种机制,编译器将无法保证大多数 Rust 程序的安全性。