在 Rust 中,Cell 和 RefCell 都是标准库提供的工具,用于实现一个非常核心的“反叛”概念:内部可变性(Interior Mutability)。
按照 Rust 的基本死规矩:如果你只拥有一个数据的不可变引用(&T),你就绝对不能修改它。但在实际开发中,我们偶尔需要在不改变对外不可变引用的前提下,偷偷修改内部的某个状态(比如记录某个方法的调用次数,或者延迟计算缓存)。
Cell 和 RefCell 就是官方提供的“合法后门”,允许你通过不可变引用去改变数据。它们的核心区别在于如何向编译器证明这种操作是安全的。
以下是详细的对比:
Cell<T>:通过“值传递”避开借用Cell 的策略非常简单粗暴:它根本不允许你获取内部数据的引用。 你要么把数据整个拿出来(拷贝走),要么用一个新数据把旧数据整个替换掉。既然没有引用,自然就不会触发借用检查器的报错。
Copy 特征的简单小数据,比如整数 (i32)、布尔值 (bool)。代码示例:
use std::cell::Cell;
fn main() {
// x 本身是不可变的(没有 mut)
let x = Cell::new(10);
// 通过不可变引用的方法 .set() 修改了它的值!
x.set(20);
// .get() 会返回内部值的拷贝(所以 T 最好实现 Copy)
println!("现在的值是: {}", x.get());
}
RefCell<T>:将借用检查推迟到“运行期”很多时候,数据太大(比如 String 或 Vec),拷贝的代价太高,或者根本没有实现 Copy。这时候就必须使用引用,Cell 就无能为力了。
RefCell 的策略是:绕过编译器的静态检查,把借用规则的检查挪到程序运行的时候(Runtime)去执行。
它在内部维护了一个计数器,当你调用 .borrow()(不可变借用)或 .borrow_mut()(可变借用)时,它会在运行时检查是否符合“多个不可变”或“唯一一个可变”的借用规则。
Copy 的复杂数据结构。panic!(崩溃)。代码示例:
use std::cell::RefCell;
fn main() {
// 依然是不可变的 binding
let data = RefCell::new(vec![1, 2, 3]);
// 获取可变引用(运行时借用检查)
let mut mutable_reference = data.borrow_mut();
mutable_reference.push(4);
// 注意:如果在这里你不手动释放 mutable_reference(或者它还没超出作用域),
// 紧接着又调用 data.borrow() 或 data.borrow_mut(),程序就会立刻崩溃!
drop(mutable_reference); // 养成好习惯,或者利用作用域限制
println!("现在的值是: {:?}", data.borrow());
}
| 特性 | Cell<T> | RefCell<T> |
| --- | --- | --- |
| 内存安全保证 | 静态保证(绝对禁止借用内部数据) | 动态保证(运行时维护借用计数) |
| 适用数据类型 | 最好实现 Copy 特征(数字、布尔等小对象) | 任何类型(String, Vec, 复杂结构体) |
| 读写数据方式 | get() 拷贝拿走,set() 整个替换 | borrow() 和 borrow_mut() 获取引用 |
| 运行时开销 | 无开销(Zero-cost) | 有轻微开销(维护借用计数器) |
| 违规后果 | 直接编译报错 | 运行时 panic!(程序崩溃) |
这里必须强调一点:Cell 和 RefCell 都是 !Sync 的,即它们绝对不能跨线程共享! * 如果需要在单线程中实现内部可变性,用 Cell / RefCell。
Mutex(对应 RefCell 的锁版本)或 RwLock,或者对于简单数字使用原子类型如 AtomicI32。