生命周期
- 是给引用用的!!! 给编译器看的!!!
- 主要是给借用检查器显式标注变量的生命周期,因为编译器没那么聪明
- 就是打个标,标一样的就以最短命的那个为准
- 生命周期(Lifetimes)是编译器用来确保所有引用(Reference)都是有效的"计时器"
- 它的核心目标是:防止悬垂指针(Dangling Pointers),即防止你访问一个已经被释放的内存地址
下面这个例子很好的展示了为什么需要声明生命周期
rust
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {result}");
}
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() { x } else { y }
// 编译器不知道具体返回的是 x 还是 y,无法确定返回值的生命周期
// 因此必须显式标注生命周期
}
// 解决方案: 在函数签名中添加生命周期注解
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}生命周期注解语法
- 注意:引用才需要生命周期注解
- 生命周期注解并不改变任何引用的生命周期的长短
- 它们描述了多个引用生命周期相互的关系,而不影响其生命周期
- 语法:
&'a,生命周期参数名称必须以撇号'开头,其名称通常全是小写,类似于泛型其名称非常短 - 大多数人使用
'a作为第一个生命周期注解
rust
&i32 // 引用
&'a i32 // 带有显式生命周期的引用
&'a mut i32 // 带有显式生命周期的可变引用函数签名中的生命周期注解
- 在函数名和参数列表间的尖括号中声明泛型生命周期(lifetime)参数,就像泛型类型(type)参数一样
- 当在函数中使用生命周期注解时,这些注解出现在函数签名中,而不存在于函数体中的任何代码中
rust
// 告诉借用检查器,限制传入的这两个参数和返回的引用存活的一样久
// 任何不满足这个约束条件的值都将被借用检查器拒绝
// 泛型生命周期 'a 的具体生命周期等同于 x 和 y 的生命周期中较小的那一个
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}生命周期省略规则
- 输入生命周期(input lifetimes): 函数或方法的参数的生命周期
- 输出生命周期(output lifetimes): 返回值的生命周期
第一条规则适用于输入生命周期,后两条规则适用于输出生命周期.
- 每个引用参数都分配一个不同的生命周期参数:
fn foo<'a>(x: &'a i32)或fn foo<'a, 'b>(x: &'a i32, y: &'b i32) - 只有一个输入生命周期参数时,将其赋予所有输出生命周期参数:
fn foo<'a>(x: &'a i32) -> &'a i32 - 方法有多个输入生命周期参数且其中一个是
&self或&mut self时,所有输出生命周期参数被赋予self的生命周期
总结:: 1) 每个参数一个名 2) 独苗传全局 3) Self 大过天
本质是为函数的返回值决定一个合适的生命周期
方法定义中的生命周期注解
- 结构体字段的生命周期必须在
impl关键字之后声明并在结构体名称之后被使用
rust
impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {announcement}");
self.part
}
}静态生命周期
- 所有的字符串字面值都拥有
'static生命周期 'static生命周期能够存活于整个程序期间(被编进二进制文件中)
rust
let s: &'static str = "I have a static lifetime.";结合泛型类型参数、trait bounds 和生命周期
rust
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 }
}额外补充
非词法作用域生命周期
Non-Lexical Lifetimes,简称NLL- 如果一个变量在之后的地方再也没被用到,那么它的借用就在它最后一次被使用的地方结束.
rust
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut *r1;
*r2 = String::from("world"); // 通过 r2 修改,没问题
dbg!(r2); // r2 的生命周期到这里结束
*r1 = String::from("world123"); // 现在 r2 不再被使用,r1 恢复了控制权
dbg!(r1);
}在旧版本的 Rust 中,变量的生命周期必须持续到大括号 } 结束.但在现在的 Rust 中,编译器会进行控制流分析:
- 在这段代码里,
dbg!(r2);是程序中最后一次出现r2的地方. - 编译器扫描后面的代码发现,第 8 行和第 9 行只用到了
r1,完全没有r2的影子. - 于是,编译器"聪明"地判断:
r2的使命已经完成了,它占用的对*r1的可变借用权限可以立即释放,归还给r1.
结构体(Struct)中的生命周期
rust
// 语义: Preview 实例的寿命('a)不能超过 content 指向的数据
struct Preview<'a> {
content: &'a str,
}
fn main() {
let text = String::from("长寿的数据");
let p = Preview { content: &text }; // 成功
}枚举(Enum)中的生命周期
rust
enum Status<'a> {
Success,
Error(&'a str), // 错误信息是借来的,不是拥有的
}Trait 实现中的生命周期
rust
// 我是为那个寿命为 'a 的 Preview 实现的打印功能
impl<'a> Display for Preview<'a> {
fn fmt(&self, f: &mut Formatter) -> Result {
write!(f, "{}", self.content)
}
}方法(Method)定义中
rust
// 结构体内部有'a引用,这个方法的寿命和'a一样
impl<'a> Preview<'a> {
// 这里的 'a 来自结构体定义
fn get_content(&self) -> &'a str {
self.content
}
}
let data = String::from("很长寿的数据");
let content_ref;
{
let p = Preview { content: &data };
content_ref = p.get_content(); // 拿到标注为 'a 的引用
} // p 在这里被销毁了
// 因为你标注了返回的是 'a(data的寿命),而不是 p 的寿命(不标'a默认是实例的寿命)
// 所以这里打印 content_ref 是安全的!编译器允许!
println!("{}", content_ref);子类型化(Subtyping)
在 Rust 编译器眼里,生命周期的比较不看大小,看包含关系.
- 符号:
'a: 'b - 读作:
'a至少活得和'b一样久('a是'b的子类型). - 算法逻辑: 如果
'a的作用域完全覆盖了'b的作用域,那么'a就可以安全地当成'b来用.