深入 unsafe Rust 与“内部可变性”:Cell、RefCell 与 UnsafeCell
📝 文章摘要
Rust 的核心价值主张是“编译时内存安全”,这由所有权和借用检查器(Borrow Checker)强制执行。然而,有时这些规则过于严格。本文将深入探讨“内部可变性”(Interior Mutability)模式,这是一种在 Rust 中安全地“绕过”编译时规则的方式。我们将剖析 UnsafeCell 作为所有内部可变性类型的构建基石,并详细对比 Cell<T>(用于 Copy 类型)和 RefCell<T>(用于运行时借用检查)的实现原理、性能开销和适用场景。
一、背景介绍
1.1 借用检查器的限制
Rust 的借用规则(“一个可变引用 &mut T”或“多个不可变引用 &T”,但不能同时存在)在编译时保证了数据竞争的安全。
// ❌ 编译错误
fn modify_while_borrowed(data: &String) {
let mut_ref = &mut data; // 错误:不能从 &String 获取 &mut String
mut_ref.push_str("!");
}
但在某些设计模式中(如图形结构、缓存、观察者模式),我们确实需要在一个对象看似“不可变”(`&self时,修改其内部的某些字段。这就是内部可变性的用武之地。
1.2 内部可变性模式
内部可变性(Interior Mutability)是一种设计模式,它允许您在拥有不可变引用(&T)时仍能修改 `T 内部的数据。

二、原理详解
2.1 一切的根源:UnsafeCell<T>
UnsafeCell<T> 是 Rust 内部可变性机制的唯一原语。
// std::cell::UnsafeCell
pub struct UnsafeCell<T> {
value: T,
}
impl<T> UnsafeCell<T> {
pub fn get(&self) -> *mut T {
// 关键:它允许从 &self (不可变引用)
// 获取 *mut T (原始可变指针)
self as *const Self as *mut T
}
}
Rust 编译器对 UnsafeCell<T> 有一个特殊的规则:**如果一个类型 T 包含 UnsafeCell<U>, T 就“不认为” &T 是真正不可变的**。Cell、RefCell、Mutex 内部都封装了 UnsafeCell。
2.2 Cell<T>:零成本的 Copy 类型可变性
`Cell<T>用于实现了 Copy Trait 的类型(如 i32, u64, bool)。
**`Cell<T>(概念上)**:
use std::cell::UnsafeCell;
pub struct Cell<T: Copy> {
value: UnsafeCell<T>,
}
impl<T: Copy> Cell<T> {
pub fn new(value: T) -> Self {
Cell { value: UnsafeCell::new(value) }
}
// 核心:set()
pub fn set(&self, value: T) {
// &self 是不可变引用
// 1. 获取 *mut T
let ptr = self.value.get();
// 2. unsafe: 写入数据
unsafe { *ptr = value; }
}
// 核心:get()
pub fn get(&self) -> T {
// 1. 获取 *mut T (即使是读取也用 *mut)
let ptr = self.value.get();
// 2. unsafe: 读取数据 (因为是 Copy,直接复制)
unsafe { *ptr }
}
}
关键点:Cell<T> *从* 给出 &T 或 &mut T。它只允许你 set(替换)或 get(复制)整个值。因为它只适用于 Copy 类型,所以没有撕裂读写(Torn Read)的风险,并且是零运行时开销的(没有锁,没有计数器)。
2.3 RefCell<T>:运行时的借用检查
RefCell<T> 适用于非 Copy 类型(如 String, Vec<T>)。它在运行时执行借用检查。
**`RefCell<T>(概念上)**:
use std::cell::{UnsafeCell, Cell};
// 借用状态
#[derive(Copy, Clone, PartialEq)]
enum BorrowState {
Readable(usize), // 有 N 个读者
Writable, // 有 1 个写者
Unused, // 未被借用
}
pub struct RefCell<T> {
value: UnsafeCell<T>,
state: Cell<BorrowState>, // 使用 Cell 来修改状态
}
impl<T> RefCell<T> {
pub fn new(value: T) -> Self {
RefCell {
value: UnsafeCell::new(value),
state: Cell::new(BorrowState::Unused),
}
}
// 获取不可变引用
pub fn borrow(&self) -> Ref<T> {
match self.state.get() {
BorrowState::Unused => {
self.state.set(BorrowState::Readable(1));
Ok(Ref::new(self))
}
BorrowState::Readable(n) => {
self.state.set(BorrowState::Readable(n + 1));
Ok(Ref::new(self))
}
BorrowState::Writable => {
panic!("RefCell: 已被可变借用"); // 运行时 Panic!
}
}.unwrap() // 简化
}
// 获取可变引用
pub fn borrow_mut(&self) -> RefMut<T> {
match self.state.get() {
BorrowState::Unused => {
self.state.set(BorrowState::Writable);
Ok(RefMut::new(self))
}
BorrowState::Readable(_) | BorrowState::Writable => {
panic!("RefCell: 已被借用"); // 运行时 Panic!
}
}.unwrap() // 简化
}
}
// Ref 和 RefMut 是智能指针,在 Drop 时恢复 state
关键点:RefCell<T> 将编译时借用检查推迟到了运行时。如果违反规则(如同时 borrow_mut 两次),程序会 panic。
三、代码实战
3.1 实战:`CellT>` (配置管理)
use std::cell::Cell;
// 全局配置,我们希望通过 &self 更新
struct AppConfig {
version: Cell<u32>,
api_key: String, // 假设 API Key 不可变
}
impl AppConfig {
// 只需要 &self
fn update_version(&self, new_version: u32) {
// 使用 .set() 修改
self.version.set(new_version);
}
fn get_version(&self) -> u32 {
// 使用 .get() 读取
self.version.get()
}
}
fn main() {
let config = AppConfig {
version: Cell::new(1),
api_key: "key_123".to_string(),
};
println!("当前版本: {}", config.get_version());
// 即使 config 是不可变的,我们也可以修改其内部的 Cell
config.update_version(2);
println!("更新后版本: {}", config.get_version());
}
3.2 实战:RefCell<T> (观察者模式)
RefCell 经常与 Rc(引用计数)结合使用,以实现多所有权的内部可变性。
use std::rc::{Rc, Weak};
use std::cell::RefCell;
// 被观察者(主题)
struct Subject<'a> {
observers: Vec<Weak<RefCell<dyn Observer + 'a>>>,
value: RefCell<i32>,
}
// 观察者 Trait
trait Observer {
fn notify(&self, value: i32);
}
impl<'a> Subject<'a> {
fn new() -> Self {
Subject { observers: Vec::new(), value: RefCell::new(0) }
}
fn attach(&mut self, observer: Weak<RefCell<dyn Observer + 'a>>) {
self.observers.push(observer);
}
fn set_value(&self, value: i32) {
// 1. 使用 RefCell 修改内部值
*self.value.borrow_mut() = value;
// 2. 通知所有观察者
for observer_weak in &self.observers {
if let Some(observer_rc) = observer_weak.upgrade() {
// 3. 观察者也使用 RefCell
observer_rc.borrow().notify(value);
}
}
}
}
// 具体的观察者
struct ConcreteObserver {
name: String,
}
impl Observer for ConcreteObserver {
fn notify(&self, value: i32) {
println!("观察者 [{}]: 收到新值 {}", self.name, value);
}
}
fn main() {
// 使用 Rc<RefCell<T>> 模式
let subject = Rc::new(RefCell::new(Subject::new()));
let observer1 = Rc::new(RefCell::new(ConcreteObserver { name: "A".to_string() }));
let observer2 = Rc::new(RefCell::new(ConcreteObserver { name: "B".to_string() }));
// 注册观察者
subject.borrow_mut().attach(Rc::downgrade(&observer1));
subject.borrow_mut().attach(Rc::downgrade(&observer2));
// 修改值,触发通知
// 我们持有的是 &Subject (通过 RefCell),但可以调用 set_value
subject.borrow().set_value(100);
}
3.3 实战:RefCell 的运行时 Panic
use std::cell::RefCell;
fn main() {
let data = RefCell::new(String::from("hello"));
// 第一次可变借用 (成功)
let mut b1 = data.borrow_mut();
b1.push_str(" world");
println!("b1: {}", b1);
// 第二次可变借用 (在 b1 释放之前)
println!("尝试第二次可变借用...");
// 触发 Panic!
// thread 'main' panicked at 'RefCell: 已被可变借用'
let b2 = data.borrow_mut();
println!("b2: {}", b2);
// 必须确保 b1 在 b2 之前被 drop
// drop(b1);
// let b2 = data.borrow_mut(); // 这才会成功
}
四、结果分析
4.1 性能与开销对比
| 类型 | 检查时机 | 线程安全 | 性能开销 | 失败后果 |
|---|---|---|---|---|
&mut T (标准) |
编译时 | 否 | 零开销 | 编译失败 |
| `Cell<T> 编译时 | 否 | 零开销 (仅限 Copy) |
N/A | |
RefCell<T> |
运行时 | 否 | 低 (原子) | Panic |
Mutex<T> |
运行时 | **是 | 高 (系统调用) | 阻塞/中毒 |
4.2 何时使用 unsafe
您不应该直接使用 UnsafeCell,除非您正在构建一个新的、安全的抽象(例如您自己的 MyMutex<T>)。Cell 和 `RefCell 已经提供了 99% 场景下的安全内部可变性封装。
使用 unsafe:
use std::cell::UnsafeCell;
// 示例:实现一个简单的原子 bool (不推荐,请用 AtomicBool)
struct MyFlag {
value: UnsafeCell<bool>,
}
unsafe impl Sync for MyFlag {} // 承诺线程安全
impl MyFlag {
// 必须使用 unsafe 块
unsafe fn set_true(&self) {
// 必须自己保证原子性
*self.value.get() = true;
}
}
五、总结与讨论
5.1 核心要点
- 内部可变性:在
&T(不可变) 引用上修改内部数据的模式。 Unsafel<T>:是所有内部可变性类型的基石,它“关闭”了编译器的\&T\不可变检查。Cell<T>:用于Copy类型,通过get()/set()复制值,零运行时开销。RefCell<T>:用于非Copy类型,通过borrow()/borrow_mut()在运行时检查借用规则,违规则 Panic。Rc<RefCell<T>>:是 Rust 中实现复杂图状数据结构或观察者模式的标准(单线程)方式。
5.2 讨论问题
RefCell<T>导致的运行时panic是否违背了 Rust“编译时安全”的承诺?Cell<T>为什么被限制为 `Copy 类型?如果不限制会发生什么?Mutex<T>和RefCell<T>有何异同?(提示:线程安全 vs 运行时检查)- 在您的项目中,哪些场景迫使您使用了
RefCell?
参考链接
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)