Rust 内部可变性
在共享引用下安全变更:Cell/RefCell 运行时借用检查;与 Mutex 的单线程/多线程分工。
#type / concept
#status / growing
#tech / dev
#resource / rust
[!info] 关联笔记
Rust 内部可变性
这个概念为什么出现
有时 API 只能拿到 &self,但仍需变更内部缓存/计数。
内部可变性把借用检查推迟到运行时(单线程 RefCell)或用原子/锁(多线程)。
[!abstract] 一句话理解
RefCell<T>允许在&self下借出&T/&mut T,违反规则则运行时 panic;Cell适合Copy的替换语义。
最小可运行示例
场景:只读服务门面下的命中计数
配置服务对外 &self 查询,内部统计命中次数。
use std::cell::RefCell;
// 业务意图:不可变方法中更新计数。
// 教学点:RefCell borrow_mut;运行时检查。
struct ConfigService {
hits: RefCell<u64>,
region: String,
}
impl ConfigService {
fn region(&self) -> &str {
*self.hits.borrow_mut() += 1;
&self.region
}
fn hits(&self) -> u64 { *self.hits.borrow() }
}
fn main() {
let svc = ConfigService { hits: RefCell::new(0), region: String::from("cn") };
println!("region={}", svc.region());
println!("hits={}", svc.hits());
}
建议运行:cargo run
期望:
region=cn
hits=1
结合场景再看三个关注点
svc本身无需 mut- 嵌套 borrow 冲突会 panic
- 多线程用 Mutex 而非 RefCell
核心概念与准确模型
| 类型 | 场景 |
|---|---|
Cell<T> | Copy 或替换 |
RefCell<T> | 单线程动态借用 |
Mutex/RwLock | 多线程 |
OnceCell/LazyLock | 延迟初始化 |
设计动机
- 表达“逻辑共享但内部记账”
- 保持外部 API 简洁
- 仍有安全兜底(panic 而非 UB)
边界与误区
- 把 RefCell 当默认 → 隐藏别名可变复杂度
- 持有
RefMut跨越危险边界 - 与
Rc组合形成图时的环
[!warning] 常见误区:RefCell 让程序在多线程自动安全 否,RefCell 不是 Sync 共享方案。
工程实践
- 优先重构所有权
- 计数/缓存等局部用内部可变
- 测试覆盖双 borrow 路径
- 多线程明确换锁
本节总结
- 内部可变 = 运行时借用规则
- Cell/RefCell/Mutex 分工
- 慎用但有价值
自测题
- RefCell 违反借用规则后果?
- 为何常与 Rc 组合?
参考答案
- 运行时 panic。
- 共享所有权节点上需要可变子字段时。
延伸阅读与资料来源
| 资料 | 类型 | 支撑内容 |
|---|---|---|
| The Book — RefCell | 官方书 | 内部可变性 |
| std::cell | 标准库 | Cell 模块 |