线程内局部可变性 ------ Cell / RefCell
一、为什么需要内部可变性
1.1 外部可变性的约束
外部可变性------&T(共享只读)和 &mut T(独占读写)不能共存------是 Rust 安全的基石。但有些合法模式无法通过这个规则表达。
1.2 三个典型场景
场景 1:引用计数
rust
use std::rc::Rc;
let r = Rc::new(42); // 通过 &Rc<i32> 访问
let r2 = Rc::clone(&r); // Rc::clone 需要递增引用计数
// ↑ 但 r 是 &Rc<i32>------不可变引用
// Rc 内部必须通过 &self 修改引用计数
场景 2:缓存统计
rust
struct Cache {
data: HashMap<u32, String>,
hits: u32,
}
impl Cache {
fn get(&self, key: u32) -> Option<&String> {
// 希望在这里更新 hits:self.hits += 1
// ❌ self 是 &self,hits 不可变
self.data.get(&key)
}
}
场景 3:测试替身
rust
struct MockService {
call_count: u32,
}
impl MockService {
fn handle(&self, request: Request) -> Response {
// 希望在这里记录调用次数
// ❌ self 是 &self
Response::ok()
}
}
共同点:需要在 &self(共享引用、不可变)下修改内部状态。
1.3 解决方案:内部可变性
-
外部可变性 :
&mut T,编译期检查,零运行时开销 -
内部可变性 :通过
Cell<T>或RefCell<T>在&self下修改,把检查推迟到运行时
| 外部可变性 | 内部可变性 | |
|---|---|---|
| 访问方式 | &mut T |
&T + Cell/RefCell |
| 检查时机 | 编译期 | 运行时 |
| 违反规则的后果 | 编译错误 | panic |
是否需要 &mut self |
是 | 否 |
1.4 为什么 C 不需要内部可变性
c
// C 中不存在"内部可变性"的概念
struct Cache {
struct hashmap* data;
int hits;
};
int cache_get(struct Cache* cache, int key) {
cache->hits++; // ✅ 任何时候都能改
// C 不存在"只能读"的引用------所有指针都可写
}
C 不需要内部可变性,因为 C 中根本没有"不能修改"的保证 。Rust 需要内部可变性,正是因为 Rust 有"不能修改"的保证(&T),而有些合法场景需要在共享引用下修改。
二、Cell<T> ------ Copy 类型的内部可变性
2.1 基本用法
rust
use std::cell::Cell;
let c = Cell::new(42);
println!("{}", c.get()); // 42
c.set(100); // 通过 &Cell<i32> 修改
println!("{}", c.get()); // 100
关键:set 和 get 都只需要 &self:
rust
impl<T> Cell<T> {
pub fn set(&self, val: T); // ← 只需 &self
pub fn get(&self) -> T where T: Copy; // ← 只需 &self
}
2.2 为什么 Cell 是安全的
rust
let c = Cell::new(42);
let r1 = &c;
let r2 = &c;
r1.set(100);
r2.set(200);
println!("{}", c.get()); // 200
安全的原因:
sql
1. T 必须是 Copy ------ set 和 get 都是直接取值/存值
2. get() 返回副本,不是内部数据的引用
3. set() 替换整个值,不产生内部引用
4. 不存在"持有内部引用时修改值"的可能性
(因为 Cell 从不返回 &T)
Cell 通过限制为 Copy 类型来保证安全。 如果 T 不是 Copy(如 String),get() 需要返回引用------那就需要 RefCell。
2.3 零成本抽象
rust
use std::mem::size_of;
println!("{}", size_of::<i32>()); // 4
println!("{}", size_of::<Cell<i32>>()); // 4 ------ 无额外开销
Cell<i32> 在内存中就是一个 i32,get/set 编译为普通的读/写指令。
2.4 常见用法
rust
// 计数器
struct Counter {
count: Cell<u32>,
}
impl Counter {
fn increment(&self) { // &self,不是 &mut self
self.count.set(self.count.get() + 1);
}
fn value(&self) -> u32 {
self.count.get()
}
}
let c = Counter::new();
let r1 = &c;
let r2 = &c;
r1.increment();
r2.increment();
assert_eq!(c.value(), 2);
rust
// 与 Rc 配合共享状态
use std::rc::Rc;
use std::cell::Cell;
let shared: Rc<Cell<i32>> = Rc::new(Cell::new(0));
let r1 = Rc::clone(&shared);
let r2 = Rc::clone(&shared);
r1.set(10);
r2.set(20);
println!("{}", shared.get()); // 20
三、RefCell<T> ------ 运行时借用检查
3.1 基本用法
rust
use std::cell::RefCell;
let rc = RefCell::new(String::from("hello"));
// 通过 borrow() 获取不可变引用
let r1 = rc.borrow(); // 返回 Ref<String>
println!("{}", r1); // "hello"
// 通过 borrow_mut() 获取可变引用
let mut r2 = rc.borrow_mut(); // 返回 RefMut<String>
r2.push_str(", world");
println!("{}", r2); // "hello, world"
3.2 签名
rust
impl<T: ?Sized> RefCell<T> {
pub fn borrow(&self) -> Ref<'_, T>;
pub fn borrow_mut(&self) -> RefMut<'_, T>;
}
两者都只需要 &self。
3.3 运行时借用规则
rust
let rc = RefCell::new(String::from("hello"));
let r1 = rc.borrow(); // ✅ 不可变引用(计数=1)
let r2 = rc.borrow(); // ✅ 另一个不可变引用(计数=2)
// let mut r3 = rc.borrow_mut(); // ❌ 运行时 panic!
// 已有不可变引用,不能借可变引用
drop(r1);
drop(r2);
let mut r3 = rc.borrow_mut(); // ✅ 现在可以了
规则与编译期相同:
-
多个
borrow()可以共存 -
borrow()和borrow_mut()不能共存 -
多个
borrow_mut()不能共存
违反规则时------panic,不是 UB。
3.4 运行时开销
rust
// RefCell 的内部结构
struct RefCell<T> {
value: T,
borrow: Cell<BorrowFlag>, // 借用计数:0 / n / -1
}
每次 borrow() / borrow_mut():
-
检查当前借用标志
-
如果合法,更新标志
-
返回
Ref/RefMut(drop 时恢复标志)
开销:一次整数检查 + 一次赋值。
3.5 Rc + RefCell:经典组合
rust
use std::rc::Rc;
use std::cell::RefCell;
let shared: Rc<RefCell<Vec<i32>>> = Rc::new(RefCell::new(vec![]));
let r1 = Rc::clone(&shared);
let r2 = Rc::clone(&shared);
r1.borrow_mut().push(1);
r2.borrow_mut().push(2);
println!("{:?}", shared.borrow()); // [1, 2]
Rc 提供共享所有权,RefCell 提供共享可变性。
3.6 panic 与 try_borrow
borrow_mut() 在违反规则时直接 panic:
rust
let rc = RefCell::new(String::from("hello"));
let r1 = rc.borrow_mut();
let r2 = rc.borrow_mut(); // ⚠️ panic: already borrowed
try_borrow_mut() 是 panic-free 版本:
rust
match rc.try_borrow_mut() {
Ok(mut r) => r.push_str("ok"),
Err(_) => println!("暂时借不到"),
}
四、&mut T vs Cell vs RefCell 对比
| 维度 | &mut T |
Cell<T> |
RefCell<T> |
|---|---|---|---|
| 检查时机 | 编译期 | 无需检查 | 运行时 |
| 违反规则 | 编译错误 | 不存在 | panic |
| T 的要求 | 任意 | Copy |
任意 |
| 获取值 | 直接 *r |
get() 返回副本 |
borrow() 返回引用 |
| 修改方式 | *r = val |
set(val) |
borrow_mut() |
| 运行时开销 | 零 | 零 | 每次 borrow 一次检查 |
能否在 &self 下用 |
❌ | ✅ | ✅ |
| 线程安全 | Send | !Sync | !Sync |
选型流程
swift
需要内部可变性(在 &self 下修改)?
│
├── T 是 Copy 吗?
│ ├── ✅ → Cell<T>(零运行时开销)
│ └── ❌ → RefCell<T>(运行时检查)
│
├── 还需要共享所有权?
│ └── ✅ → Rc<RefCell<T>>
│
└── 还需要跨线程?
└── → 见「跨线程可变性」章节
五、与 C 程序员的对话
"C 不需要这些,因为所有指针都能写"
C 程序员:"在 C 中你任何时候都能写任何数据。Cell 和 RefCell 在 C 中没意义。"
Rust :"这正是关键区别。C 中所有数据都是可写的,所以 C 中不存在'共享不可变'的保证。Rust 有&T保证------'这个引用指向的数据不会被任何人修改'。但有些场景(缓存、计数)确实需要在共享引用下修改,所以 Rust 提供了 Cell 和 RefCell 作为受控的例外。"
c
// C ------ 没有"共享不可变"
const struct Cache* cache = get_cache();
// 谁能保证没人修改它?另一个线程可能有非 const 指针指向同一内存
rust
// Rust ------ &T 是真正的只读
let cache: &Cache = get_cache();
// 编译器保证不会有人修改 cache 的数据
// 但如果字段用了 Cell/RefCell,那些字段可以变
"运行时检查不就是开销吗?"
Rust :"是开销。但大部分代码用
&mut T(编译期检查,零开销)。只有在确实需要在&self下修改时才使用 RefCell。C 中没有 borrow 检查的开销,但有调试内存 bug 的开销------那通常比 borrow 检查贵得多。"
erlang
95% 的代码:&mut T → 零运行时开销
5% 的代码:Cell → 零运行时开销(仅限 Copy)
<1% 的代码:RefCell → 有运行时开销
六、小结
核心概念
r
外部可变性:&mut T --- 编译期检查,零开销,独占访问
↓ 不够用时:
线程内内部可变性:
├── Cell<T> --- 仅 Copy 类型,零开销,取值/存值
└── RefCell<T> --- 任意类型,运行时借用检查
C vs Rust
| C | Rust | |
|---|---|---|
| 共享引用时能改吗 | 可以(所有指针都可写) | 不能(&T 保证),除非用 Cell/RefCell |
| 内部可变性 | 不需要(一切皆可改) | 需要(因为 &T 保证不可改) |
| 违反规则的后果 | 无(也没有规则) | 编译错误(&mut T)或 panic(RefCell) |
一句话总结 :Rust 通过
&T提供了真正的"共享不可变"保证,但有些场景需要在共享引用下修改数据。Cell和RefCell在单线程中提供这种能力:Cell用于Copy类型(零开销),RefCell用于任意类型(运行时检查)。它们把 C 中"一切皆可写"的混乱变成"默认不可写,显式声明即可写"的秩序。