Day 10: 生命周期
今天是《30天精通Rust语言》系列的第十天,也是基础部分最后一章。我们将学习生命周期(Lifetime)。生命周期是Rust中最独特的概念之一,它确保引用在使用的过程中始终有效,防止悬垂引用等安全问题。
2. 什么是生命周期
Section titled “2. 什么是生命周期”生命周期是Rust编译器用于追踪引用有效性的机制。当我们创建一个引用时,该引用有一个生命周期——它在此期间保持有效。
fn main() { let r; // 声明一个引用,生命周期开始 { let x = 5; r = &x; // r引用x,x的有效期在此作用域内 } // x离开作用域,被drop println!("r: {}", r); // 编译错误!r引用的数据已不存在}这段代码会编译失败,因为Rust检测到 r 引用了已离开作用域的 x。生命周期注解帮助编译器确认引用的有效性。
3. 生命周期注解
Section titled “3. 生命周期注解”生命周期注解描述了引用生命周期的关系,不改变引用的有效期。
3.1 语法
Section titled “3.1 语法”生命周期注解使用单引号开头的小写标识符:
&'a i32 // 生命周期'a的引用&'mut arr // 生命周期'arr的可变引用3.2 函数签名中的生命周期
Section titled “3.2 函数签名中的生命周期”当函数返回引用时,需要标注其生命周期:
// 不标注生命周期会编译错误fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y }}
fn main() { let s1 = String::from("long string"); let s2 = String::from("xyz"); let result = longest(s1.as_str(), s2.as_str()); println!("The longest string is '{}'", result);}<'a> 声明了一个生命周期参数 'a,表明输入引用的生命周期必须至少为 'a',返回引用也具有生命周期 'a'。
4. 结构体中的生命周期
Section titled “4. 结构体中的生命周期”结构体中持有引用时,必须标注生命周期:
struct ImportantExcerpt<'a> { part: &'a str,}
fn main() { let novel = String::from("Call me Ishmael. Some years ago..."); let first_sentence = novel.split('.').next().unwrap();
let excerpt = ImportantExcerpt { part: first_sentence, }; println!("Excerpt: {}", excerpt.part);}ImportantExcerpt<'a> 意味着结构体实例不能比它引用的数据存活更久。
5. 生命周期省略规则
Section titled “5. 生命周期省略规则”Rust团队发现某些模式下生命周期可以由编译器推断,无需手动标注。这些规则称为”生命周期消除规则”(Lifetime Elision Rules)。
5.1 省略规则
Section titled “5.1 省略规则”规则一:每个引用参数都有独立的生命周期。
// 编译器推断为 fn foo<'a>(&'a i32)fn foo(x: &i32) -> &i32 { x }规则二:如果只有一个输入生命周期,自动赋给输出生命周期。
// 编译器推断为 fn foo<'a>(&'a i32) -> &'a i32fn foo(x: &i32) -> &i32 { x }规则三:如果方法中有一个 &self 或 &mut self,该生命周期赋给所有输出生命周期。
5.2 需要手动标注的情况
Section titled “5.2 需要手动标注的情况”当编译器无法推断生命周期时,必须手动标注:
// 多个输入生命周期,返回引用与其中一个关联fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y }}
// 不能确定返回引用与哪个输入关联fn first_word<'a>(s: &'a str) -> &'a str { s.split(' ').next().unwrap_or("")}6. 静态生命周期
Section titled “6. 静态生命周期”'static 是特殊的生命周期,表示在整个程序运行期间有效:
fn main() { let s: &'static str = "I have a static lifetime."; println!("{}", s);}字符串字面量默认是 'static 生命周期,因为它们直接存储在程序二进制中,始终可用。
7. 方法中的生命周期
Section titled “7. 方法中的生命周期”当结构体方法涉及引用时,需要使用生命周期:
struct Quote<'a> { content: &'a str,}
impl<'a> Quote<'a> { fn new(content: &'a str) -> Quote<'a> { Quote { content } }
// &self的生命周期自动与返回的&str关联 fn content(&self) -> &str { self.content }
fn who(&self) -> &str { "Unknown" }}
fn main() { let q = Quote::new("The only way to do great work is to love what you do."); println!("Quote: {}", q.content());}8. 多个生命周期
Section titled “8. 多个生命周期”当函数有多个引用且它们之间无关时,可以声明多个生命周期:
fn mix<'a, 'b>(x: &'a str, y: &'b str) -> &'b str { if x.len() > y.len() { x // 这里返回'a,但函数签名声明返回'b,编译错误 } else { y }}这个例子编译失败,因为返回类型声明了 'b,但 x 的生命周期是 'a。修正:
fn mix<'a>(x: &'a str, y: &str) -> &'a str { if x.len() > y.len() { x } else { y // y被提升为'a生命周期 }}9. 生命周期与所有权
Section titled “9. 生命周期与所有权”生命周期与所有权是独立但互补的概念:
- 所有权:决定值的生命周期和何时被drop。
- 生命周期:追踪引用有效性的编译时检查。
fn main() { let mut s = String::from("hello"); let r1 = &s; // r1借用s let r2 = &s; // r2借用s println!("{} and {}", r1, r2); // r1和r2在此后不再使用,可以安全地创建可变引用
let r3 = &mut s; // 可变借用 r3.push_str(", world"); println!("{}", r3);}借用规则与生命周期协同工作,确保内存安全。
10. 实践练习
Section titled “10. 实践练习”今天的实践任务:
- 编写程序验证悬垂引用,创建一个返回局部变量引用的函数,观察编译错误。
- 实现
longest函数,验证不同生命周期参数的用法。 - 创建一个持有引用的结构体
Paragraph<'a>,实现其方法。 - 编写函数
first_sentence提取字符串的第一个句子,添加生命周期注解。 - 分析一段代码(可以是标准库代码),识别生命周期省略规则的应用。
11. 常见问题
Section titled “11. 常见问题”问:什么时候需要手动标注生命周期?
答:当函数有多个引用参数且返回引用时,编译器需要知道你返回的引用与哪个输入关联,此时需要手动标注。
问:生命周期会改变引用的行为吗?
答:不会。生命周期完全是编译时概念,不产生任何运行时开销。它只是告诉编译器引用的有效期,以便进行借用检查。
12. 总结
Section titled “12. 总结”今天我们完成了Rust核心概念的学习:
- 生命周期追踪引用的有效性,防止悬垂引用。
- 生命周期注解用
'语法声明,如'a。 - 函数和结构体中的引用需要正确标注生命周期。
- Rust的省略规则在简单情况下自动推断生命周期。
- 静态生命周期
'static表示整个程序运行期间有效。
恭喜你完成了Rust基础部分的学习!这十天涵盖了Rust最核心的概念:所有权、借用、生命周期。这些概念是Rust独特性的根基,掌握它们将让你能够编写安全、高效的Rust代码。
接下来的二十天,我们将深入更多高级主题,包括泛型、trait、集合、迭代器、并发等。继续保持学习的热情!