第二章:借用检查器与生命周期

第二章:借用检查器与生命周期

生命周期是 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 是在告诉编译器:输入引用和输出引用之间的关联关系,让它能验证安全性。

何时需要显式生命周期

  1. 函数有多个引用参数,且返回引用(编译器无法确定返回值和哪个参数相关)
  2. 结构体包含引用字段
  3. 方法返回引用(且不是来自 self

生命周期的实际工程影响: 大多数 Rust 应用代码不需要写很多生命周期标注。你只会在编写通用库或有复杂引用关系的数据结构时频繁遇到。日常业务代码中,使用 clone() 或重新组织数据结构往往更简洁。

“生命周期不是 Rust 的负担,是 Rust 帮你验证代码正确性的工具。当编译器要求你添加生命周期标注时,它实际上是在说:‘我无法确定这段代码是安全的,你来帮我确认。’”