借用检查器原理
本章目标: 深入理解借用规则和生命周期系统,能读懂并修复借用检查器错误
学习时间: 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
解读步骤:
- 找到不可变借用的位置 (
&data[0]) - 找到冲突的可变借用 (
data.push) - 找到不可变借用最后使用的位置 (
r) - 修复: 要么在 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
解读步骤:
- 找到第一个可变借用
- 找到第二个可变借用
- 修复: 确保第一个借用结束后才开始第二个
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 的最好队友! 🦀