借用检查器原理

本章目标: 深入理解借用规则和生命周期系统,能读懂并修复借用检查器错误

学习时间: 5-7 天

前置知识: 完成 02-所有权与内存模型.md


借用: 不转移所有权的引用

借用(Borrowing)允许你使用一个值而不获取其所有权。Rust 的借用规则保证了内存安全的同时,消除了数据竞争。


1. 两种借用类型

不可变借用 (&T)

fn main() {
    let s = String::from("hello");
    
    let r1 = &s;  // 不可变借用
    let r2 = &s;  // 可以同时有多个不可变借用!
    
    println!("{} and {}", r1, r2);
    // r1 和 r2 的生命周期在这里结束
    
    println!("{}", s);  // s 仍然有效
}

可变借用 (&mut T)

fn main() {
    let mut s = String::from("hello");
    
    let r = &mut s;  // 可变借用
    r.push_str(", world");
    // r 的生命周期在这里结束
    
    println!("{}", s);  // 可以使用 s 了
}

借用规则

规则 1: 在任意时刻,你可以拥有:
   - 任意数量的不可变引用 (&T), 或者
   - 恰好一个可变引用 (&mut T)
   
规则 2: 引用必须总是有效的(不能是悬垂引用)

2. 为什么这些规则存在?

没有规则会发生什么?

// 示例: 迭代器失效 (Go 中也可能发生)
// 如果 Rust 允许这样写:
fn invalid_demo_not_real_rust() {
    let mut v = vec![1, 2, 3];
    
    for item in &v {         // 不可变借用 v
        v.push(*item);       // 尝试可变借用 v - 错误!
        // 如果允许: push 可能触发重新分配,
        // 迭代器的内部指针就变成了悬垂指针!
    }
}

读写分离模型

同时读 (多个 &T):  线程安全 ✓
独占写 (&mut T):  修改安全 ✓
同时读写:         数据竞争 ✗

这与读写锁的概念完全一致,只是 Rust 在编译期静态保证了这一点。


3. 生命周期 (Lifetimes)

生命周期的概念

每个引用在 Rust 中都有一个生命周期 —— 引用有效的作用域范围。

{
    let x = 5;              // x 的生命周期开始
    
    let r;                  // r 的生命周期开始
    {
        let y = 10;         // y 的生命周期开始
        r = &y;             // r 借用 y
    }                       // y 的生命周期结束, y 被 drop
    
    // println!("{}", r);   // 编译错误! r 引用了已释放的 y
                            // y 的生命周期比 r 短
}

生命周期标注语法

// 生命周期参数以 ' 开头
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

什么时候需要标注?

// 不需要标注: 只有一个引用参数
fn first_word(s: &str) -> &str {
    &s[..s.find(' ').unwrap_or(s.len())]
}
 
// 需要标注: 多个引用参数, 返回引用
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    // 编译器无法推断返回值的生命周期来自 x 还是 y
    if x.len() > y.len() { x } else { y }
}
 
// 不需要标注: 返回值生命周期显然来自 self
impl MyStruct {
    fn get_data(&self) -> &str {
        &self.data  // 生命周期和 &self 一样
    }
}

4. 借用检查器的工作原理

非词法生命周期 (NLL)

现代 Rust 使用 NLL (Non-Lexical Lifetimes),生命周期基于实际使用,而不仅是词法作用域:

fn nll_demo() {
    let mut s = String::from("hello");
    
    let r = &s;             // 不可变借用开始
    println!("{}", r);      // r 最后一次使用
    // r 的生命周期在这里结束 (NLL)
    
    s.push_str(" world");   // 现在可以可变借用了!
    println!("{}", s);
}

旧版 Rust (词法作用域):

// 这段代码在旧版 Rust 中无法编译!
fn old_rust_fail() {
    let mut s = String::from("hello");
    {
        let r = &s;         // 不可变借用
        println!("{}", r);
    }                       // r 的作用域结束
    s.push_str(" world");   // 在旧版需要大括号才行
}

借用检查器如何工作

// 借用检查器在 MIR (Middle Intermediate Representation) 上运行
 
fn borrowck_analysis() {
    let mut data = vec![1, 2, 3];
    
    // MIR 中借用检查器追踪:
    // 1. 变量的 "locations" (代码点)
    // 2. 每个借用的生命周期范围
    // 3. 借用冲突检测
    
    let x = &data[0];    // 借用 data, 生命周期: L1..L3
    println!("{}", x);   // L2: 使用 x
    // x 不再使用, 生命周期结束域: L3
    
    data.push(4);        // L4: 修改 data, 此时 x 已无效, OK
}

5. 常见借用错误与解决方案

错误 1: 借用冲突

// 错误代码
fn borrow_conflict() {
    let mut v = vec![1, 2, 3];
    let first = &v[0];      // 不可变借用
    v.push(4);              // 可变借用 - 冲突!
    println!("{}", first);
}
 
// 解决方案 1: 先完成不可变借用
fn fix_1() {
    let mut v = vec![1, 2, 3];
    let first_val = v[0];   // 复制值, 不借用
    v.push(4);
    println!("{}", first_val);
}
 
// 解决方案 2: 克隆
fn fix_2() {
    let mut v = vec![1, 2, 3];
    let first = v[0].clone();
    v.push(4);
    println!("{}", first);
}
 
// 解决方案 3: 调整顺序 (使用 NLL)
fn fix_3() {
    let mut v = vec![1, 2, 3];
    {
        let first = &v[0];
        println!("{}", first);
    }  // first 借用结束
    v.push(4);
}

错误 2: 悬垂引用

// 错误代码: 返回局部变量的引用
fn dangle() -> &str {
    let s = String::from("hello");
    &s  // s 离开作用域被 drop, 引用悬垂!
}
 
// 解决方案: 返回值而非引用
fn no_dangle() -> String {
    let s = String::from("hello");
    s  // 移动所有权给调用方
}

错误 3: 借用已移动的值

// 错误代码
fn borrow_after_move() {
    let s = String::from("hello");
    let s2 = s;           // s moved
    println!("{}", &s);   // 错误: s 已移动
}
 
// 解决方案: 克隆或借用
fn fix() {
    let s = String::from("hello");
    let s2 = s.clone();   // 克隆
    println!("{}", &s);   // 正常
    println!("{}", &s2);  // 正常
}

错误 4: 可变引用不能有别名

// 错误代码
fn multiple_mut_refs() {
    let mut s = String::from("hello");
    let r1 = &mut s;
    let r2 = &mut s;  // 错误: 不能有两个可变引用
    println!("{} {}", r1, r2);
}
 
// 解决方案: 顺序使用
fn sequential_mut_refs() {
    let mut s = String::from("hello");
    
    {
        let r1 = &mut s;
        r1.push_str(" world");
    }  // r1 结束
    
    let r2 = &mut s;
    r2.push('!');
    
    println!("{}", s);
}

6. 生命周期标注详解

函数生命周期

// 基础: 返回值生命周期受限于两个参数中较短的那个
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}
 
// 不同生命周期: 返回值只依赖 x
fn first<'a, 'b>(x: &'a str, _y: &'b str) -> &'a str {
    x
}
 
// 测试
fn main() {
    let string1 = String::from("long string is long");
    let result;
    {
        let string2 = String::from("xyz");
        result = longest(string1.as_str(), string2.as_str());
        println!("Longest: {}", result);  // 在 string2 有效期内使用, OK
    }
    // println!("Longest: {}", result);  // 错误: string2 已无效
}

结构体生命周期

// 结构体包含引用时, 必须标注生命周期
struct Important<'a> {
    part: &'a str,
}
 
impl<'a> Important<'a> {
    fn level(&self) -> i32 {
        3
    }
    
    fn announce(&self, announcement: &str) -> &str {
        println!("Attention: {}", announcement);
        self.part
    }
}
 
fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence;
    
    {
        let i = Important {
            part: novel.split('.').next().expect("Could not find '.'"),
        };
        first_sentence = i.part;
        println!("Important: {}", i.announce("something important"));
    }
    // i 离开作用域, 但 first_sentence 仍有效 (借用的是 novel)
    println!("{}", first_sentence);
}

生命周期省略规则 (Lifetime Elision)

Rust 有三条规则,在明确时省略生命周期标注:

// 规则 1: 每个引用参数都有独立的生命周期
fn rule1(x: &str, y: &str) 
// 等同于:
fn rule1<'a, 'b>(x: &'a str, y: &'b str)
 
// 规则 2: 只有一个输入引用, 其生命周期赋给输出
fn rule2(x: &str) -> &str
// 等同于:
fn rule2<'a>(x: &'a str) -> &'a str
 
// 规则 3: 有 &self 或 &mut self, 其生命周期赋给输出
impl Struct {
    fn rule3(&self) -> &str
    // 等同于:
    fn rule3<'a>(&'a self) -> &'a str
}

7. 高级生命周期

’static 生命周期

// 'static 表示整个程序运行期间都有效
let s: &'static str = "hello world";  // 字符串字面量
 
// 函数接受 'static 引用
fn needs_static(r: &'static str) {
    println!("{}", r);
}
 
// trait 对象中的 'static
fn make_closure() -> Box<dyn Fn() + 'static> {
    let x = 5;
    // 注意: 闭包捕获了 x, 但 x 是 Copy 的, 所以是 'static
    Box::new(move || println!("{}", x))
}

生命周期子类型

// 'a: 'b 意味着 'a 的生命周期至少和 'b 一样长
fn complex<'a: 'b, 'b>(x: &'a str, y: &'b str) -> &'b str {
    if x.len() > y.len() { x } else { y }
}

where 子句中的生命周期

use std::fmt::Display;
 
fn longest_with_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 }
}

8. 借用检查器进阶: 内部可变性

有时候你需要在不可变引用中修改数据。Rust 提供了 Cell 和 RefCell 来实现内部可变性:

Cell

use std::cell::Cell;
 
struct SomeStruct {
    regular_field: u8,
    special_field: Cell<u8>,  // 允许通过不可变引用修改
}
 
fn main() {
    let my_struct = SomeStruct {
        regular_field: 0,
        special_field: Cell::new(1),
    };
    
    // 通过不可变引用修改 Cell 内容
    let new_value = my_struct.special_field.get() + 1;
    my_struct.special_field.set(new_value);  // OK! 即使 my_struct 是不可变的
    
    println!("{}", my_struct.special_field.get());  // 2
}

RefCell

use std::cell::RefCell;
 
#[derive(Debug)]
struct Node {
    value: i32,
    children: RefCell<Vec<Node>>,  // 运行时借用检查
}
 
impl Node {
    fn new(value: i32) -> Self {
        Node { value, children: RefCell::new(vec![]) }
    }
    
    fn add_child(&self, child: Node) {
        self.children.borrow_mut().push(child);  // 运行时获取可变借用
    }
}
 
fn main() {
    let root = Node::new(1);
    root.add_child(Node::new(2));  // 通过不可变引用添加子节点
    root.add_child(Node::new(3));
    
    println!("{:?}", root);
    
    // RefCell 的借用在运行时检查
    let borrow1 = root.children.borrow();      // 不可变借用
    // let borrow2 = root.children.borrow_mut(); // 运行时 panic! 已有不可变借用
}

9. 编译器错误信息解读

理解 E0502

error[E0502]: cannot borrow `data` as mutable because it is also borrowed as immutable
  --> src/main.rs:5:5
   |
4  |     let r = &data[0];
   |              ---- immutable borrow occurs here
5  |     data.push(4);
   |     ^^^^^^^^^^^^ mutable borrow occurs here
6  |     println!("{}", r);
   |                    - immutable borrow later used here

解读步骤:

  1. 找到不可变借用的位置 (&data[0])
  2. 找到冲突的可变借用 (data.push)
  3. 找到不可变借用最后使用的位置 (r)
  4. 修复: 要么在 push 之前不再使用 r, 要么克隆 r 的值

理解 E0499

error[E0499]: cannot borrow `s` as mutable more than once at a time
  --> src/main.rs:5:14
   |
4  |     let r1 = &mut s;
   |              ------ first mutable borrow occurs here
5  |     let r2 = &mut s;
   |              ^^^^^^ second mutable borrow occurs here
6  |     println!("{} {}", r1, r2);
   |                       -- first borrow later used here

解读步骤:

  1. 找到第一个可变借用
  2. 找到第二个可变借用
  3. 修复: 确保第一个借用结束后才开始第二个

10. 实战: 借用检查器友好的设计

设计原则

// 原则 1: 优先借用而非克隆
fn process_string(s: &str) -> usize {  // &str 而非 String
    s.len()
}
 
// 原则 2: 切片优于 Vec
fn sum_slice(data: &[i32]) -> i32 {  // &[i32] 比 &Vec<i32> 更通用
    data.iter().sum()
}
 
// 原则 3: 分离读写操作
struct Cache {
    data: Vec<String>,
}
 
impl Cache {
    fn get(&self, idx: usize) -> Option<&str> {  // 不可变借用
        self.data.get(idx).map(|s| s.as_str())
    }
    
    fn insert(&mut self, s: String) {  // 可变借用
        self.data.push(s);
    }
}

练习题

练习 1 (基础): 借用规则判断

以下代码会编译吗?

fn main() {
    let mut s = String::from("hello");
    
    let r1 = &s;
    let r2 = &s;
    let r3 = &mut s;
    
    println!("{}, {}, {}", r1, r2, r3);
}

答案: 不会编译。同时存在不可变借用 (r1, r2) 和可变借用 (r3)。修复:

fn main() {
    let mut s = String::from("hello");
    
    let r1 = &s;
    let r2 = &s;
    println!("{}, {}", r1, r2);  // r1, r2 使用结束
    
    let r3 = &mut s;  // 现在可以可变借用
    r3.push_str(" world");
    println!("{}", r3);
}

练习 2 (基础): 生命周期分析

fn get_longest(s1: &str, s2: &str) -> &str {
    if s1.len() >= s2.len() { s1 } else { s2 }
}
// 这个函数需要生命周期标注吗?为什么?

答案: 需要。因为返回值可能来自 s1 或 s2,编译器无法推断返回值的生命周期。

fn get_longest<'a>(s1: &'a str, s2: &'a str) -> &'a str {
    if s1.len() >= s2.len() { s1 } else { s2 }
}

练习 3 (中等): 修复借用错误

fn main() {
    let mut v = vec![1, 2, 3, 4, 5];
    
    let first = v.first().unwrap();
    
    for i in 0..v.len() {
        v[i] *= 2;  // 错误!
    }
    
    println!("First was: {}", first);
}

答案:

fn main() {
    let mut v = vec![1, 2, 3, 4, 5];
    
    // 先获取值, 而非引用
    let first_val = *v.first().unwrap();
    
    for i in 0..v.len() {
        v[i] *= 2;
    }
    
    println!("First was: {}", first_val);
    println!("After: {:?}", v);
}

练习 4 (中等): 结构体生命周期

为以下结构体添加正确的生命周期标注:

struct Excerpt {
    text: &str,    // 需要生命周期
}
 
impl Excerpt {
    fn new(text: &str) -> Excerpt {
        Excerpt { text }
    }
    
    fn get_text(&self) -> &str {
        self.text
    }
}

答案:

struct Excerpt<'a> {
    text: &'a str,
}
 
impl<'a> Excerpt<'a> {
    fn new(text: &'a str) -> Excerpt<'a> {
        Excerpt { text }
    }
    
    fn get_text(&self) -> &str {  // 生命周期省略规则: 返回 &self 的生命周期
        self.text
    }
}

练习 5 (中等): RefCell 使用

实现一个使用 RefCell 的观察者模式:

use std::cell::RefCell;
 
trait Observer {
    fn update(&self, value: i32);
}
 
struct Counter {
    count: RefCell<i32>,
}
 
impl Observer for Counter {
    fn update(&self, _value: i32) {
        *self.count.borrow_mut() += 1;
    }
}
 
struct Subject {
    observers: Vec<Box<dyn Observer>>,
    value: i32,
}
 
impl Subject {
    fn new() -> Self {
        Subject { observers: vec![], value: 0 }
    }
    
    fn subscribe(&mut self, observer: Box<dyn Observer>) {
        self.observers.push(observer);
    }
    
    fn set_value(&mut self, value: i32) {
        self.value = value;
        for observer in &self.observers {
            observer.update(value);
        }
    }
}
 
fn main() {
    let mut subject = Subject::new();
    let counter = Counter { count: RefCell::new(0) };
    subject.subscribe(Box::new(counter));
    
    subject.set_value(1);
    subject.set_value(2);
    subject.set_value(3);
    
    // 如何访问 count? 提示: 需要修改设计
}

练习 6 (中等): 解读编译错误

解释以下代码的编译错误:

fn main() {
    let mut data = vec![1, 2, 3];
    
    for x in &data {
        if *x == 1 {
            data.retain(|&v| v != 1);
        }
        println!("{}", x);
    }
}

答案: for x in &data 创建了对 data 的不可变借用。data.retain(...) 需要可变借用。同时存在这两种借用违反借用规则。

修复方案:

fn main() {
    let mut data = vec![1, 2, 3];
    
    // 方案1: 先过滤, 再迭代
    data.retain(|&v| v != 1);
    for x in &data {
        println!("{}", x);
    }
    
    // 方案2: 收集要处理的元素
    let to_remove: Vec<i32> = data.iter()
        .filter(|&&v| v == 1)
        .cloned()
        .collect();
    for v in &to_remove {
        data.retain(|x| x != v);
    }
}

练习 7 (挑战): 实现 LRU 缓存占位符

使用 RefCell 实现一个简单的 LRU 缓存访问计数器:

use std::cell::RefCell;
use std::collections::HashMap;
 
struct AccessTracker {
    access_counts: RefCell<HashMap<String, u32>>,
}
 
impl AccessTracker {
    fn new() -> Self {
        AccessTracker {
            access_counts: RefCell::new(HashMap::new()),
        }
    }
    
    // 通过不可变引用记录访问
    fn record_access(&self, key: &str) {
        *self.access_counts.borrow_mut()
            .entry(key.to_string())
            .or_insert(0) += 1;
    }
    
    fn get_count(&self, key: &str) -> u32 {
        *self.access_counts.borrow()
            .get(key)
            .unwrap_or(&0)
    }
    
    fn top_accessed(&self, n: usize) -> Vec<(String, u32)> {
        let mut items: Vec<(String, u32)> = self.access_counts
            .borrow()
            .iter()
            .map(|(k, &v)| (k.clone(), v))
            .collect();
        items.sort_by(|a, b| b.1.cmp(&a.1));
        items.truncate(n);
        items
    }
}
 
fn main() {
    let tracker = AccessTracker::new();
    
    tracker.record_access("page1");
    tracker.record_access("page1");
    tracker.record_access("page2");
    tracker.record_access("page1");
    tracker.record_access("page3");
    tracker.record_access("page2");
    
    println!("page1: {} times", tracker.get_count("page1"));
    
    println!("Top 2:");
    for (key, count) in tracker.top_accessed(2) {
        println!("  {}: {}", key, count);
    }
}

练习 8 (挑战): 自限制借用

实现一个 SplitMut 工具,允许同时借用切片的两个不重叠部分:

fn split_at_mut_safe<T>(slice: &mut [T], idx: usize) -> (&mut [T], &mut [T]) {
    let len = slice.len();
    assert!(idx <= len);
    
    // 使用 unsafe 实现两个不重叠部分的可变借用
    let ptr = slice.as_mut_ptr();
    unsafe {
        (
            std::slice::from_raw_parts_mut(ptr, idx),
            std::slice::from_raw_parts_mut(ptr.add(idx), len - idx),
        )
    }
}
 
fn main() {
    let mut v = vec![1, 2, 3, 4, 5, 6];
    
    let (left, right) = split_at_mut_safe(&mut v, 3);
    
    // 同时修改两个部分
    left[0] = 10;
    right[0] = 40;
    
    println!("{:?}", v);  // [10, 2, 3, 40, 5, 6]
}

练习 9 (挑战): 复杂生命周期场景

// 实现一个缓存, 存储计算结果
struct Cache<'a, T> {
    data: &'a mut Vec<T>,
    computed: bool,
}
 
impl<'a, T: Clone> Cache<'a, T> {
    fn new(data: &'a mut Vec<T>) -> Self {
        Cache { data, computed: false }
    }
    
    fn compute(&mut self) -> &[T] {
        if !self.computed {
            // 模拟计算
            self.computed = true;
        }
        self.data.as_slice()
    }
}
 
fn main() {
    let mut storage = vec![1, 2, 3];
    let mut cache = Cache::new(&mut storage);
    
    let result = cache.compute();
    println!("{:?}", result);
    
    // 问题: 下面这行能编译吗?
    // println!("{:?}", storage);  // 为什么?
}

分析: cache 持有 &'a mut storage 的可变借用。在 cache 的生命周期内, storage 不能被再次借用。只有当 cache drop 之后,才能再次使用 storage。


练习 10 (综合): 文本分析器

实现一个文本分析器,展示借用检查器在实际场景中的应用:

use std::collections::HashMap;
 
struct TextAnalyzer<'a> {
    text: &'a str,
}
 
impl<'a> TextAnalyzer<'a> {
    fn new(text: &'a str) -> Self {
        TextAnalyzer { text }
    }
    
    fn word_count(&self) -> usize {
        self.text.split_whitespace().count()
    }
    
    fn char_frequency(&self) -> HashMap<char, usize> {
        let mut freq = HashMap::new();
        for c in self.text.chars() {
            if c.is_alphabetic() {
                *freq.entry(c.to_lowercase().next().unwrap()).or_insert(0) += 1;
            }
        }
        freq
    }
    
    fn sentences(&self) -> Vec<&str> {
        self.text.split(|c| c == '.' || c == '!' || c == '?')
            .map(|s| s.trim())
            .filter(|s| !s.is_empty())
            .collect()
    }
    
    fn most_common_words(&self, n: usize) -> Vec<(&str, usize)> {
        let mut word_count: HashMap<&str, usize> = HashMap::new();
        for word in self.text.split_whitespace() {
            *word_count.entry(word).or_insert(0) += 1;
        }
        
        let mut pairs: Vec<(&str, usize)> = word_count.into_iter().collect();
        pairs.sort_by(|a, b| b.1.cmp(&a.1));
        pairs.truncate(n);
        pairs
    }
}
 
fn main() {
    let text = "The quick brown fox jumps over the lazy dog. \
                The dog slept. The fox ran away!";
    
    let analyzer = TextAnalyzer::new(text);
    
    println!("单词数: {}", analyzer.word_count());
    println!("句子数: {}", analyzer.sentences().len());
    
    println!("最常见的 3 个词:");
    for (word, count) in analyzer.most_common_words(3) {
        println!("  '{}': {} 次", word, count);
    }
}

练习 11 (挑战): 生命周期变型

理解生命周期中的协变和逆变:

// 协变 (covariant): 可以使用更长生命周期的引用替换
fn covariant<'a, 'b: 'a>(x: &'b str) -> &'a str {
    x  // 'b >= 'a, 可以安全地缩短生命周期
}
 
// 不变 (invariant): 不能替换
use std::cell::Cell;
fn invariant_example<'a>(x: Cell<&'a str>) -> Cell<&'a str> {
    x  // Cell<&T> 是不变的!
}
 
// 为什么 Cell 是不变的?
// 因为 Cell 允许内部可变性, 如果允许协变:
// Cell<&'long str> → Cell<&'short str>
// 可能写入 &'short str, 但有更长生命周期的代码读取它 → 悬垂引用!

练习 12 (综合): 借用检查器 vs Go 的 GC

实现同一个算法,对比 Go 和 Rust 的内存模型差异:

// Rust 版本: 图遍历(借用检查器友好的设计)
use std::collections::{HashMap, VecDeque};
 
struct Graph {
    edges: HashMap<usize, Vec<usize>>,
}
 
impl Graph {
    fn new() -> Self {
        Graph { edges: HashMap::new() }
    }
    
    fn add_edge(&mut self, from: usize, to: usize) {
        self.edges.entry(from).or_insert_with(Vec::new).push(to);
    }
    
    // 返回借用结果, 不分配额外内存
    fn neighbors(&self, node: usize) -> &[usize] {
        self.edges.get(&node)
            .map(|v| v.as_slice())
            .unwrap_or(&[])
    }
    
    fn bfs(&self, start: usize) -> Vec<usize> {
        let mut visited = Vec::new();
        let mut queue = VecDeque::new();
        let mut seen = std::collections::HashSet::new();
        
        queue.push_back(start);
        seen.insert(start);
        
        while let Some(node) = queue.pop_front() {
            visited.push(node);
            
            for &neighbor in self.neighbors(node) {
                if seen.insert(neighbor) {
                    queue.push_back(neighbor);
                }
            }
        }
        
        visited
    }
}
 
fn main() {
    let mut g = Graph::new();
    g.add_edge(0, 1);
    g.add_edge(0, 2);
    g.add_edge(1, 3);
    g.add_edge(2, 3);
    g.add_edge(3, 4);
    
    let order = g.bfs(0);
    println!("BFS order: {:?}", order);
}

小结

概念要点
不可变借用任意数量,同时读
可变借用独占一个,读写
NLL生命周期基于实际使用,不是词法块
生命周期标注多个引用参数时,显式说明依赖关系
RefCell运行时借用检查,实现内部可变性

下一步

  • 完成本章所有练习题
  • 阅读 04-类型系统与Trait.md
  • 尝试在 Rust Playground 故意触发各种借用错误

借用检查器不是你的敌人,而是帮你找出潜在 bug 的最好队友! 🦀