Skip to content

借用检查器

> 理解借用检查器的工作原理,掌握 NLL(非词法生命周期)的概念,学会解读和修复常见借用错误。

借用检查器的工作原理

> 借用检查器(Borrow Checker)是 Rust 编译器的核心组件,在编译时检查所有引用的安全性。

借用检查器是什么?

┌─────────────────────────────────────────────────────┐
│              借用检查器工作流程                      │
├─────────────────────────────────────────────────────┤
│                                                     │
│   源代码                                            │
│     ↓                                              │
│   编译器分析                                        │
│     ↓                                              │
│   ┌─────────────────────┐                          │
│   │   借用检查器         │                          │
│   │ • 追踪每个引用的作用域│                          │
│   │ • 检查借用规则       │                          │
│   │ • 验证生命周期       │                          │
│   └─────────────────────┘                          │
│     ↓          ↓                                   │
│   ✅ 通过    ❌ 错误                                │
│   编译成功   编译失败 + 详细错误信息                 │
│                                                     │
└─────────────────────────────────────────────────────┘

生命周期追踪

借用检查器使用"生命周期"概念追踪引用的有效范围:

rust
fn main() {
    let s = String::from("hello");  // ── s 开始 ──────────┐
                                    //                      │
    let r1 = &s;                     // ── r1 开始 ────┐   │
                                     //                 │   │
    println!("{}", r1);              //                 │   │
                                     // ── r1 结束 ─────┘   │
    let r2 = &mut s;                 // ── r2 开始 ──────┐ │
                                     //                  │ │
    r2.push_str(" world");           //                  │ │
                                     // ── r2 结束 ──────┘ │
    println!("{}", s);               //                      │
}                                    // ── s 结束 ──────────┘
▶ Run

借用规则可视化

┌─────────────────────────────────────────────────────┐
│              引用规则可视化                          │
├─────────────────────────────────────────────────────┤
│                                                     │
│  ✅ 允许:多个不可变引用                             │
│  ┌────────────────────────────────────┐            │
│  │  let s = String::from("hello");    │            │
│  │  let r1 = &s;  // 不可变引用 1     │            │
│  │  let r2 = &s;  // 不可变引用 2     │            │
│  │  let r3 = &s;  // 不可变引用 3     │            │
│  │  println!("{}, {}, {}", r1, r2, r3);│           │
│  └────────────────────────────────────┘            │
│                                                     │
│  ✅ 允许:一个可变引用(独占)                       │
│  ┌────────────────────────────────────┐            │
│  │  let mut s = String::from("hi");   │            │
│  │  let r = &mut s; // 唯一可变引用   │            │
│  │  r.push_str("!");                  │            │
│  │  println!("{}", r);                │            │
│  └────────────────────────────────────┘            │
│                                                     │
│  ❌ 禁止:可变 + 不可变引用同时存在                 │
│  ┌────────────────────────────────────┐            │
│  │  let mut s = String::from("hi");   │            │
│  │  let r1 = &s;      // 不可变引用   │            │
│  │  let r2 = &mut s;  // ❌ 编译错误  │            │
│  │  println!("{}, {}", r1, r2);       │            │
│  └────────────────────────────────────┘            │
│                                                     │
│  ❌ 禁止:多个可变引用同时存在                      │
│  ┌────────────────────────────────────┐            │
│  │  let mut s = String::from("hi");   │            │
│  │  let r1 = &mut s;  // 可变引用 1   │            │
│  │  let r2 = &mut s;  // ❌ 编译错误  │            │
│  │  r1.push_str("a");                 │            │
│  │  r2.push_str("b");                 │            │
│  └────────────────────────────────────┘            │
│                                                     │
└─────────────────────────────────────────────────────┘

借用作用域(Borrow Scope)

关键概念:NLL(Non-Lexical Lifetimes)

Rust 2018+ 使用非词法生命周期,引用在最后一次使用后结束,而非作用域结束:

rust
fn main() {
    let mut s = String::from("hello");
    
    let r1 = &s;
    println!("{}", r1);  // r1 最后使用位置
    
    // r1 借用在此结束(NLL),即使作用域未结束
    
    let r2 = &mut s;     // ✅ 允许:r1 已不再使用
    r2.push_str(" world");
    println!("{}", r2);
}
▶ Run

NLL vs 传统作用域

传统作用域(旧 Rust):
┌────────────────────────────────────┐
│ let mut s = String::from("hello");│
│ let r1 = &s;                       │─── r1 开始
│ println!("{}", r1);                │
│                                    │
│ let r2 = &mut s;  // ❌ 错误       │─── r1 仍在作用域
│                                    │─── r1 结束
└────────────────────────────────────┘

NLL(Rust 2018+):
┌────────────────────────────────────┐
│ let mut s = String::from("hello");│
│ let r1 = &s;                       │─── r1 开始
│ println!("{}", r1);                │─── r1 最后使用
│                                    │─── r1 借用结束
│ let r2 = &mut s;  // ✅ 允许       │─── r2 开始
│ r2.push_str(" world");             │
│ println!("{}", r2);                │─── r2 结束
└────────────────────────────────────┘

常见借用错误案例

案例 1:迭代时修改

rust
// ❌ 错误:迭代时修改集合
fn main() {
    let mut numbers = vec![1, 2, 3, 4, 5];
    
    for num in &numbers {           // 不可变借用开始
        if *num % 2 == 0 {
            numbers.push(*num * 2); // ❌ 可变借用冲突
        }
    }
}
▶ Run

错误信息:

error[E0502]: cannot borrow `numbers` as mutable because it is also borrowed as immutable
 --> src/main.rs:5:13
  |
4 |     for num in &numbers {
  |                 -------- immutable borrow occurs here
5 |         if *num % 2 == 0 {
6 |             numbers.push(*num * 2);
  |             ^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here

修复方案:

rust
// ✅ 方案 1:收集后修改
fn main() {
    let mut numbers = vec![1, 2, 3, 4, 5];
    let evens: Vec<i32> = numbers.iter()
        .filter(|&&x| x % 2 == 0)
        .map(|&x| x * 2)
        .collect();
    
    numbers.extend(evens);
    println!("{:?}", numbers);
}

// ✅ 方案 2:使用索引
fn main() {
    let mut numbers = vec![1, 2, 3, 4, 5];
    let len = numbers.len();
    
    for i in 0..len {
        if numbers[i] % 2 == 0 {
            numbers.push(numbers[i] * 2);
        }
    }
}
▶ Run

案例 2:结构体字段借用冲突

rust
// ❌ 错误:同时借用可变和不可变字段
struct User {
    name: String,
    age: u32,
}

fn main() {
    let mut user = User {
        name: String::from("Alice"),
        age: 25,
    };
    
    let name_ref = &user.name;       // 不可变借用整个 user
    user.age = 26;                    // ❌ 可变借用冲突
    
    println!("{}", name_ref);
}
▶ Run

错误信息:

error[E0502]: cannot borrow `user` as mutable because it is also borrowed as immutable
 --> src/main.rs:11:5
  |
10|     let name_ref = &user.name;
  |                     ---- immutable borrow occurs here
11|     user.age = 26;
  |     ^^^^^^^^^^^^^^ mutable borrow occurs here

修复方案:

rust
// ✅ 方案 1:先使用,再修改
fn main() {
    let mut user = User {
        name: String::from("Alice"),
        age: 25,
    };
    
    let name = user.name.clone();    // 克隆数据
    user.age = 26;                    // ✅ 现在可以修改
    
    println!("{}", name);
}

// ✅ 方案 2:分离结构体字段
fn main() {
    let mut user = User {
        name: String::from("Alice"),
        age: 25,
    };
    
    let name_ref = &user.name;       // 只借用 name 字段
    println!("{}", name_ref);        // 先使用
    
    user.age = 26;                    // ✅ 修改 age 字段
}
▶ Run

案例 3:方法调用链中的借用

rust
// ❌ 错误:方法链中的临时借用
fn main() {
    let mut v = vec![1, 2, 3];
    
    let first = v.first().unwrap();  // 不可变借用
    v.push(4);                        // ❌ 可变借用冲突
    
    println!("{}", first);
}
▶ Run

错误信息:

error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
 --> src/main.rs:5:5
  |
4 |     let first = v.first().unwrap();
  |                 - immutable borrow occurs here
5 |     v.push(4);
  |     ^^^^^^^^^ mutable borrow occurs here

修复方案:

rust
// ✅ 方案:先复制值
fn main() {
    let mut v = vec![1, 2, 3];
    
    let first = *v.first().unwrap(); // 复制值(i32 是 Copy 类型)
    v.push(4);                        // ✅ 现在可以修改
    
    println!("{}", first);
}
▶ Run

案例 4:闭包中的借用

rust
// ❌ 错误:闭包捕获可变引用
fn main() {
    let mut list = vec![1, 2, 3];
    
    let mut push_to_list = || {
        list.push(4);                 // 闭包可变借用 list
    };
    
    println!("{:?}", list);          // ❌ 不可变借用冲突
    push_to_list();
}
▶ Run

修复方案:

rust
// ✅ 方案:控制借用顺序
fn main() {
    let mut list = vec![1, 2, 3];
    
    println!("{:?}", list);          // 先使用
    
    let mut push_to_list = || {
        list.push(4);
    };
    push_to_list();
    
    println!("{:?}", list);          // ✅ 闭包已执行完毕
}
▶ Run

案例 5:返回引用到已释放的数据

rust
// ❌ 错误:返回局部变量的引用
fn get_biggest(numbers: &Vec<i32>) -> &i32 {
    let mut biggest = numbers[0];
    
    for &num in numbers.iter().skip(1) {
        if num > biggest {
            biggest = num;            // biggest 是局部变量
        }
    }
    
    // &biggest  // ❌ 返回局部变量引用
}

// ✅ 正确:返回输入数据的引用
fn get_biggest(numbers: &Vec<i32>) -> &i32 {
    let mut biggest = &numbers[0];
    
    for num in numbers.iter().skip(1) {
        if num > biggest {
            biggest = num;
        }
    }
    
    biggest
}

fn main() {
    let nums = vec![1, 5, 3, 9, 2];
    let max = get_biggest(&nums);
    println!("最大值:{}", max);
}
▶ Run

借用检查器错误信息解读

错误类型对照表

错误代码含义常见原因
E0502同时存在可变和不可变引用读时写、写时读
E0499多次可变借用同时创建多个可变引用
E0503可变引用后使用原变量借用时访问所有者
E0505不可变引用后可变借用借用未结束时修改
E0515返回悬垂引用返回局部变量引用
E0597引用生命周期不足引用比数据活得更久

详细错误分析示例

rust
fn main() {
    let mut s = String::from("hello");
    
    let r1 = &s;
    let r2 = &mut s;        // ❌ 错误行
    
    println!("{}, {}", r1, r2);
}
▶ Run

完整错误信息解读:

error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
 --> src/main.rs:4:14
  |
3 |     let r1 = &s;
  |              -- immutable borrow occurs here
  |              │
  |              不可变借用发生在这里
4 |     let r2 = &mut s;
  |              ^^^^^^ mutable borrow occurs here
  |              │
  |              可变借用发生在这里
5 |
6 |     println!("{}, {}", r1, r2);
  |                        -- immutable borrow later used here
  |                        │
  |                        不可变借用在此处被使用

解读步骤:

  1. 错误代码E0502 表示可变和不可变引用冲突
  2. 错误位置:第 4 行尝试创建可变引用
  3. 冲突源:第 3 行已有不可变引用 &s
  4. 引用使用:第 6 行使用了 r1(不可变引用)
  5. 根本原因r1 在第 6 行仍在使用,与第 4 行的可变借用冲突

实际应用场景

场景 1:构建器模式

rust
#[derive(Debug)]
struct QueryBuilder {
    table: String,
    conditions: Vec<String>,
}

impl QueryBuilder {
    fn new(table: &str) -> Self {
        QueryBuilder {
            table: table.to_string(),
            conditions: Vec::new(),
        }
    }
    
    fn add_condition(&mut self, condition: &str) -> &mut Self {
        self.conditions.push(condition.to_string());
        self                                       // 返回可变引用
    }
    
    fn build(&self) -> String {
        let where_clause = if self.conditions.is_empty() {
            String::new()
        } else {
            format!(" WHERE {}", self.conditions.join(" AND "))
        };
        
        format!("SELECT * FROM {}{}", self.table, where_clause)
    }
}

fn main() {
    let mut builder = QueryBuilder::new("users");
    
    // 链式调用(可变引用链)
    builder
        .add_condition("age > 18")
        .add_condition("active = true")
        .add_condition("country = 'CN'");
    
    let query = builder.build();
    println!("{}", query);
    // SELECT * FROM users WHERE age > 18 AND active = true AND country = 'CN'
}
▶ Run

场景 2:事件处理系统

rust
#[derive(Debug)]
struct Event {
    name: String,
    data: String,
}

struct EventHandler {
    events: Vec<Event>,
}

impl EventHandler {
    fn new() -> Self {
        EventHandler { events: Vec::new() }
    }
    
    fn add_event(&mut self, name: &str, data: &str) {
        self.events.push(Event {
            name: name.to_string(),
            data: data.to_string(),
        });
    }
    
    fn process_events(&mut self) {
        // 可变迭代处理事件
        for event in &mut self.events {
            event.data = format!("[PROCESSED] {}", event.data);
        }
    }
    
    fn print_events(&self) {
        // 不可变迭代显示事件
        for event in &self.events {
            println!("{}: {}", event.name, event.data);
        }
    }
}

fn main() {
    let mut handler = EventHandler::new();
    
    // 添加事件(可变借用)
    handler.add_event("click", "button_id=123");
    handler.add_event("submit", "form_id=456");
    
    // 显示事件(不可变借用)
    println!("初始事件:");
    handler.print_events();
    
    // 处理事件(可变借用)
    handler.process_events();
    
    // 再次显示(不可变借用)
    println!("\n处理后:");
    handler.print_events();
}
▶ Run

场景 3:缓存与计算

rust
use std::collections::HashMap;

struct Cache {
    data: HashMap<String, i32>,
    hits: u32,
    misses: u32,
}

impl Cache {
    fn new() -> Self {
        Cache {
            data: HashMap::new(),
            hits: 0,
            misses: 0,
        }
    }
    
    fn get(&mut self, key: &str) -> Option<i32> {
        match self.data.get(key) {        // 不可变借用
            Some(&value) => {
                self.hits += 1;           // 可变借用(分时进行)
                Some(value)
            }
            None => {
                self.misses += 1;
                None
            }
        }
    }
    
    fn set(&mut self, key: &str, value: i32) {
        self.data.insert(key.to_string(), value);
    }
    
    fn stats(&self) -> (u32, u32) {
        (self.hits, self.misses)
    }
}

fn main() {
    let mut cache = Cache::new();
    
    // 设置值(可变借用)
    cache.set("a", 1);
    cache.set("b", 2);
    
    // 获取值(可变借用)
    println!("a = {:?}", cache.get("a"));  // Some(1)
    println!("c = {:?}", cache.get("c"));  // None
    
    // 查看统计(不可变借用)
    let (hits, misses) = cache.stats();
    println!("命中:{},未命中:{}", hits, misses);
}
▶ Run

调试借用问题的技巧

技巧 1:添加显式作用域

rust
// 疑难问题时,显式标注借用范围
fn main() {
    let mut s = String::from("hello");
    
    {
        let r = &s;
        println!("{}", r);
    }  // r 借用结束
    
    {
        let r = &mut s;
        r.push_str(" world");
        println!("{}", r);
    }  // r 借用结束
    
    println!("{}", s);  // ✅ 清晰的借用分离
}
▶ Run

技巧 2:克隆数据

rust
// 不确定借用关系时,克隆是简单方案
fn main() {
    let mut data = vec![1, 2, 3, 4, 5];
    
    let first = data.first().unwrap().clone();  // 克隆值
    data.push(6);                                // ✅ 可以修改
    
    println!("first = {}, data = {:?}", first, data);
}
▶ Run

技巧 3:使用索引代替迭代

rust
// 迭代器借用时的替代方案
fn main() {
    let mut numbers = vec![10, 20, 30, 40, 50];
    
    // ❌ 迭代器借用冲突
    // for num in &numbers {
    //     if *num > 25 {
    //         numbers.push(*num);
    //     }
    // }
    
    // ✅ 使用索引避免借用冲突
    let len = numbers.len();
    for i in 0..len {
        if numbers[i] > 25 {
            numbers.push(numbers[i]);
        }
    }
    
    println!("{:?}", numbers);
}
▶ Run

技巧 4:分离读和写

rust
// 将读操作和写操作分离
fn main() {
    let mut users = vec![
        String::from("Alice"),
        String::from("Bob"),
    ];
    
    // 先读取需要的数据
    let names_to_add: Vec<String> = users.iter()
        .filter(|name| name.starts_with('A'))
        .cloned()
        .collect();
    
    // 再修改数据
    users.extend(names_to_add);
    
    println!("{:?}", users);
}
▶ Run

小结

借用检查器关键概念

  1. 生命周期追踪:编译器追踪每个引用的有效范围
  2. NLL:引用在最后一次使用后结束,而非作用域结束
  3. 借用规则:同一时刻只能有多个不可变引用或一个可变引用

常见错误模式

错误模式解决方案
迭代时修改收集后修改 / 使用索引
字段借用冲突克隆 / 分离操作
方法链临时借用提前复制值
闭包捕获控制执行顺序
返回悬垂引用返回所有权 / 输入引用

调试策略

  1. 添加显式作用域
  2. 必要时克隆数据
  3. 使用索引代替迭代器
  4. 分离读和写操作

核心思想

借用检查器虽然严格,但保证了内存安全和并发安全。理解它的工作原理,能帮助写出更安全的 Rust 代码。


练习题

详见:练习题