第二章:借用检查器与生命周期
第二章:借用检查器与生命周期
生命周期是 Rust 中最让初学者头疼的概念。好消息是:大多数情况下编译器可以自动推断生命周期。你只需要在编译器需要帮助时,告诉它"这个引用至少活多久"。
一、借用规则
引用规则(同一时间,对同一值):
规则 1:可以有无限多个不可变引用(&T)
规则 2:只能有一个可变引用(&mut T)
规则 3:不可变引用和可变引用不能同时存在
这些规则防止数据竞争(data race):
如果只有不可变引用,所有人都可以安全读
如果有可变引用,是独占访问,不会有并发问题
fn main() {
let mut s = String::from("hello");
// ✓ 多个不可变引用同时存在
let r1 = &s;
let r2 = &s;
println!("{} {}", r1, r2); // ok
// 注意:r1 和 r2 最后一次使用在上面那行之后就"失效"了
// Rust 2018+ 的非词法作用域生命周期(NLL)让引用在最后一次使用后就结束
// ✓ r1, r2 使用后可以创建可变引用
let r3 = &mut s;
println!("{}", r3); // ok
}
二、生命周期的概念
// 这个函数有问题:
// 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 的含义:返回的引用的生命周期,至少和 x 和 y 中较短的那个一样长
// 这只是告诉编译器"输入和输出之间的关系",不是改变运行时行为
fn main() {
let string1 = String::from("long string");
{
let string2 = String::from("xy");
let result = longest(string1.as_str(), string2.as_str());
println!("{}", result); // ok,result 不超过 string2 的生命周期
}
// result 在这里无效(string2 已经被 drop)
}
三、生命周期省略规则
// 很多情况下不需要显式标注,编译器可以推断
// 规则 1:每个输入引用获得独立的生命周期参数
fn first_word(s: &str) -> &str { // 等价于 fn first_word<'a>(s: &'a str) -> &'a str
let bytes = s.as_bytes();
for (i, &byte) in bytes.iter().enumerate() {
if byte == b' ' {
return &s[..i];
}
}
s
}
// 规则 2:只有一个输入引用时,输出引用继承该生命周期
// 规则 3:方法中,如果有 &self,输出引用继承 self 的生命周期
struct Important {
content: String,
}
impl Important {
fn content(&self) -> &str { // 等价于 fn content<'a>(&'a self) -> &'a str
&self.content
}
}
四、结构体中的生命周期
// 结构体包含引用时,需要生命周期标注
struct Excerpt<'a> {
content: &'a str, // 该结构体实例不能超过这个引用的生命周期
}
fn main() {
let text = String::from("Call me Ishmael. Some years ago...");
let first_sentence = text.split('.').next().expect("找不到 '.'");
let excerpt = Excerpt {
content: first_sentence,
};
println!("{}", excerpt.content);
}
// text 和 excerpt 在同一作用域,所以 ok
五、'static 生命周期
// 'static 表示可以活跃到程序结束
// 字符串字面量都是 'static,因为它们编译进二进制文件
let s: &'static str = "I have a static lifetime.";
// 通常不需要手动写 'static
// 如果编译器要求你加 'static,通常说明有更深层的设计问题
六、智能指针与所有权
// Box<T>:堆上分配,单一所有权
let b = Box::new(5);
println!("{}", b); // 自动解引用
// 递归类型需要 Box
#[derive(Debug)]
enum List {
Cons(i32, Box<List>),
Nil,
}
let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
// Rc<T>:引用计数,多个所有者(只适合单线程)
use std::rc::Rc;
let a = Rc::new(5);
let b = Rc::clone(&a); // 增加引用计数,不是 deep clone
let c = Rc::clone(&a);
println!("引用计数:{}", Rc::strong_count(&a)); // 3
// 注意:Rc<T> 不能用于多线程(不是 Send)
// 多线程用 Arc<T>(原子引用计数)
// RefCell<T>:运行时借用检查(内部可变性)
use std::cell::RefCell;
let data = RefCell::new(vec![1, 2, 3]);
{
let mut v = data.borrow_mut(); // 运行时借用检查
v.push(4);
}
println!("{:?}", data.borrow()); // [1, 2, 3, 4]
// 注意:如果同时有两个 borrow_mut(),运行时 panic(不是编译时)
// 慎用,只在确实需要内部可变性时使用
七、常见模式:Rc<RefCell>
use std::rc::Rc;
use std::cell::RefCell;
// 共享可变状态(单线程)的标准模式
#[derive(Debug)]
struct Node {
value: i32,
children: Vec<Rc<RefCell<Node>>>,
}
fn main() {
let node1 = Rc::new(RefCell::new(Node { value: 1, children: vec![] }));
let node2 = Rc::new(RefCell::new(Node { value: 2, children: vec![] }));
// node2 可以有多个"所有者"(parent 和其他引用)
node1.borrow_mut().children.push(Rc::clone(&node2));
println!("{:?}", node1.borrow());
}
// 多线程等价:Arc<Mutex<T>>
关键认知
生命周期的心理模型:
生命周期不是"这个值活多久",而是"这个引用指向的值至少要活多久"。标注 'a 是在告诉编译器:输入引用和输出引用之间的关联关系,让它能验证安全性。
何时需要显式生命周期:
- 函数有多个引用参数,且返回引用(编译器无法确定返回值和哪个参数相关)
- 结构体包含引用字段
- 方法返回引用(且不是来自
self)
生命周期的实际工程影响:
大多数 Rust 应用代码不需要写很多生命周期标注。你只会在编写通用库或有复杂引用关系的数据结构时频繁遇到。日常业务代码中,使用 clone() 或重新组织数据结构往往更简洁。
“生命周期不是 Rust 的负担,是 Rust 帮你验证代码正确性的工具。当编译器要求你添加生命周期标注时,它实际上是在说:‘我无法确定这段代码是安全的,你来帮我确认。’”