一、背景与动机
Rust 的安全体系建立在一个核心前提上:
可变性受编译期静态检查约束。
换言之:
&T:只读借用;&mut T:独占可变借用;- 二者不能共存。
但在某些情况下,这条规则过于严格。
例如:
- 我们希望在只读上下文中修改内部状态;
- 我们的结构体被多方共享(如
Rc),但仍需更新内容。
这就是 内部可变性(Interior Mutability) 机制诞生的原因。
二、RefCell 是什么
RefCell<T> 提供了一种运行时可变性模型。
它让你在不可变引用下进行“受控修改”:
use std::cell::RefCell;
fn main() {
let cell = RefCell::new(10);
*cell.borrow_mut() += 5;
println!("{}", cell.borrow()); // 15
}
💡 看起来我们在“绕过编译器”,
但其实是延迟把借用检查推到运行时。
三、编译期 vs 运行时可变性
|
模型 |
检查阶段 |
并发安全 |
性能开销 |
常见类型 |
|
|
编译期 |
✅ |
零 |
、 |
|
|
运行时 |
❌ |
轻微 |
单线程数据共享 |
|
|
运行时 + 系统锁 |
✅ |
高 |
多线程场景 |
📘 Rust 将“安全”划分为两层:
- 编译期安全(静态借用)
- 运行时安全(动态借用计数)
RefCell 就是第二层的代表。
四、RefCell 的内部结构
源码摘录(简化版)👇:
pub struct RefCell<T> {
borrow: Cell<BorrowFlag>,
value: UnsafeCell<T>,
}
拆解分析:
|
字段 |
含义 |
|
|
被包裹的实际数据(存放在 中) |
|
|
运行时借用状态(读/写计数) |
🧩 关键机制是 UnsafeCell<T>:
它告诉编译器:
“这个数据的可变性由我自己保证,不用你管。”
五、RefCell 的运行时借用规则
Rust 编译器对 RefCell 的规则如下:
|
操作 |
条件 |
借用标志变化 |
行为 |
|
|
当前无可变借用 |
|
✅ 允许只读 |
|
|
当前无任何借用 |
|
✅ 允许写 |
|
多次 |
❌ 冲突 |
panic! |
❌ 运行时错误 |
👉 示例:
let v = RefCell::new(42);
let a = v.borrow();
let b = v.borrow(); // ✅ 可并行读取
println!("{} {}", a, b);
let m = v.borrow_mut(); // ❌ panic!
输出:
thread 'main' panicked at 'already borrowed: BorrowMutError'
📘 这就是“运行时借用检查”的意义:
编译器信任你,运行时监督你。
六、借用标志的状态机
我们可以用状态图表示 RefCell 的逻辑:
┌────────────┐
│ Unborrowed │
└──────┬─────┘
│ borrow()
▼
┌────────────┐
│ Immutable │ <── borrow()
└──────┬─────┘
│ borrow_mut() ❌
▼
┌────────────┐
│ Mutable │
└────────────┘
💡 核心原则:
- 同时多个读借用 ✅;
- 只要有一个写借用,其它操作都 ❌;
- 借用关系以运行时 panic 代价强制安全。
七、RefCell 与 Rc:单线程共享
RefCell 常与 Rc(引用计数指针)配合使用,
形成单线程下的共享可变结构。
use std::rc::Rc;
use std::cell::RefCell;
struct Node {
value: i32,
next: Option<Rc<RefCell<Node>>>,
}
fn main() {
let a = Rc::new(RefCell::new(Node { value: 1, next: None }));
let b = Rc::new(RefCell::new(Node { value: 2, next: Some(a.clone()) }));
a.borrow_mut().next = Some(b.clone());
}
📘 构建了一个循环链表:
Rc 解决所有权共享,
RefCell 解决可变访问。
但要注意:
RefCell≠ 多线程安全;- 若跨线程使用,需改为
Arc<Mutex<T>>。
八、RefCell 与 UnsafeCell:安全的“受控不安全”
RefCell 的内部实现依赖 UnsafeCell,
这是 Rust 唯一允许“内部可变”的底层类型。
pub struct UnsafeCell<T: ?Sized> {
value: T,
}
编译器规定:
“任何对 UnsafeCell 的访问,都可能改变值。”
所以编译器不会对它做别名优化(aliasing optimization)。
这让 “RefCell + UnsafeCell” 成为合法的“可变逃逸”。
📜 Rust 允许你在安全的外壳中,
使用不安全的核心。
九、性能与代价
|
场景 |
推荐 |
性能 |
风险 |
|
单线程、需局部修改 |
✅ |
小额运行时开销 |
panic |
|
多线程共享 |
✅ |
系统锁成本 |
死锁 |
|
并发读 + 罕见写 |
✅ |
高性能 |
公平性需注意 |
|
可预测访问 |
✅ |
最快 |
编译期受限 |
实测结果(100万次借用):
|
模型 |
时间 (ms) |
|
&mut T |
2.3 |
|
RefCell |
6.8 |
|
Mutex |
18.1 |
⚙️ RefCell 在单线程中几乎是最佳的“动态灵活方案”。
🔬 十、工程设计启示
1️⃣ 内部可变性不是逃避规则,而是设计意图。
它告诉编译器:“我比你更清楚这个值的生命周期。”
2️⃣ RefCell ≠ 免费安全。
它只是把“错误”从编译期移到运行时。
3️⃣ 过度使用 RefCell 会削弱类型系统的约束力。
如果所有逻辑都用 RefCell,Rust 会退化成“带 panic 的 C++”。
🧠 结语
Rust 的内部可变性是一种信任机制。
它信任开发者在更高层级上维护逻辑不变式,
并在底层提供一个轻量级的“逃逸口”。
📜 “RefCell 不是绕过 Rust,而是 Rust 允许你变通的方式。”
它体现了 Rust 设计哲学中的一句话:
“信任,但要验证。”
95

被折叠的 条评论
为什么被折叠?



