RefCell<T> 与内部可变性
本教程共 78 篇 · 第 63 篇 · 更新于 2026-08-08 · 约 10 分钟阅读
本节目标:理解”内部可变性”——在值被不可变引用绑定时仍能修改它;掌握 RefCell
把借用检查从编译期推迟到运行期的机制、违背规则会运行时 panic,以及用 Rc<RefCell > 实现”多主人且可修改”。
Rust 的编译期借用检查保证了安全,但也带来死板:要么一个可变借用,要么多个不可变借用,二者不能共存。可现实中有些设计就是需要”表面只读、内部可变”。RefCell<T> 正是为此而生,它用”内部可变性”破了这个局。
1-1 什么是内部可变性
正常情况下,这是非法的:
fn main() {
let x = 5;
let y = &mut x; // 报错:不能对不可变值做可变借用
}
但内部可变性允许:RefCell<T> 包裹的值,即使外层绑定是 &self(不可变),里面也能改。代价是借用规则的检查从编译期移到了运行期。
Note内部可变性靠
unsafe实现,但都被封装在安全的 API 后面,你写业务代码时完全感知不到 unsafe。Rust 的哲学是:把不安全锁在小黑屋,对外只露安全接口。
1-2 RefCell 的基本用法
RefCell 用 borrow 拿不可变借用、borrow_mut 拿可变借用:
use std::cell::RefCell;
fn main() {
let s = RefCell::new(String::from("hello"));
let s1 = s.borrow();
let s2 = s.borrow_mut(); // 编译期不报错,但运行期会 panic
println!("{},{}", s1, s2);
}
注意:这段代码编译能过,但一运行就崩:
thread 'main' panicked at 'already borrowed: BorrowMutError'
因为同一时刻既借了不可变又借了可变,违背借用规则。RefCell 没有在编译期拦你,而是运行期直接 panic。从编译器错误变成了运行时异常——规则并没有被绕过,只是迟到。
1-3 为什么需要 RefCell
你可能会问:宁可编译期报错,何必拖到运行期?原因在于 Rust 编译器”宁可错杀、绝不放过”——当你确定代码正确、它却误判时报错,你就需要 RefCell 来告诉编译器”相信我”。
典型场景:一个外部库定义的特征,方法签名是 &self(不可变),但你的实现需要在里面修改自己的字段。
pub trait Messenger {
fn send(&self, msg: String);
}
pub struct MsgQueue {
msg_cache: RefCell<Vec<String>>,
}
impl Messenger for MsgQueue {
fn send(&self, msg: String) {
self.msg_cache.borrow_mut().push(msg); // 在 &self 里改了字段
}
}
send 的签名是 &self,不能改成 &mut self(那是库定死的)。但 msg_cache 用 RefCell 包着,于是 &self 内部照样能 push。这就是内部可变性的核心价值:在不破坏外部签名的前提下,悄悄允许内部修改。
Tip当你确信借用关系没问题、编译器却死活不让你编译时,先想是不是该用
RefCell。但别滥用——能用普通&mut解决的,就别上RefCell,因为运行期 panic 比编译期报错更难查。
1-4 Cell 与 RefCell 怎么选
Cell<T> 和 RefCell<T> 都提供内部可变性,区别在于:
Cell<T>适用于实现了Copy的值,用get/set取值赋值。RefCell<T>适用于引用(没实现Copy的类型),用borrow/borrow_mut。
use std::cell::Cell;
fn main() {
let c = Cell::new("asdf"); // &str 实现了 Copy
let one = c.get();
c.set("qwer");
let two = c.get();
println!("{},{}", one, two);
}
若 Cell::new(String::from("x")) 会报错:String 没实现 Copy。这种场景改用 RefCell。开发中 RefCell 更常用,因为我们要改的往往是结构体、字符串这类非 Copy 类型。
性能上:Cell 零额外开销;RefCell 内部多一个字节的”借用状态”指示器,每次借用时更新它,有一点点运行期开销。所以非用内部可变性时,优先 Cell,类型不满足才上 RefCell。
Warning
Cell永远不会 panic,因为它不提供引用、只提供值的拷贝;RefCell会 panic,因为它提供引用、可能触发借用冲突。选哪个,看你要值还是引用。
1-5 组合:Rc<RefCell>
Rc 给多所有权,RefCell 给可修改,两者一组合就是”多个主人共享且能改同一份数据”:
use std::cell::RefCell;
use std::rc::Rc;
fn main() {
let s = Rc::new(RefCell::new(String::from("我很善变")));
let s1 = Rc::clone(&s);
let s2 = Rc::clone(&s);
s2.borrow_mut().push_str(",还拥有多个主人");
println!("{:?}", s);
println!("{:?}", s1);
println!("{:?}", s2);
}
s、s1、s2 三个 Rc 是同一份数据的所有者。任一个通过 borrow_mut 修改,其余看到的都同步变化——因为它们共享底层。输出三段都是改后的内容。
这对组合性能其实不差,大致相当于没有线程安全版本的 C++ shared_ptr。内存上只比裸数据多几个计数整数;CPU 上 clone、drop、borrow 都是几次加减比较。热点路径实在不放心,做个基准测试即可。
1-6 内部可变性的本质与安全边界
内部可变性不是魔法,它只是把”借用是否合法”的判断推迟了。Rust 保证:只要在某个时刻同时出现可变借用与(其他)不可变借用,运行期必 panic。它不会让不安全的借用悄悄通过——只是把刹车从编译期挪到了运行期。
这种机制适合两类情况:一是你确定代码对、编译器误报;二是某个值被分散在各处读写,借用关系太复杂难以在编译期理清(Rust 编译器自身大量使用 RefCell 管理内部 map 就是例证)。
Note
RefCell只用于单线程。多线程下共享可变状态要用Mutex<T>或RwLock<T>——它们把”运行时借用检查”升级成了”真正的锁”,既安全又能跨线程。这块留到并发章节细讲。
1-7 小结
- 内部可变性:不可变绑定下仍能改内部数据,检查从编译期延到运行期。
Cell给 Copy 值、RefCell给引用;后者冲突时运行期 panic。Rc<RefCell<T>>是”多主人 + 可修改”的经典组合。
Warning
RefCell的 panic 只在运行期暴露,单元测试覆盖不到的分支可能藏着借用冲突。用它时确保修改和读取不会在同一作用域同时发生可变与不可变借用。
1-8 内部可变性的边界与误用
内部可变性是把双刃剑,用对了优雅,用错了埋雷。几条边界要守住:
第一,能用普通 &mut 解决的,别上 RefCell。RefCell 把检查挪到运行期,意味着编译期那层”早于运行就拦住你”的保护没了。能编译期保证的安全,永远比运行期 panic 可靠。RefCell 只该用于”编译期实在理不清借用、但你确信运行期不会冲突”的少数地方。
第二,RefCell 不是线程安全的。它的借用计数只是普通整数,多线程下并发 borrow 会数据竞争、直接未定义行为。跨线程共享可变状态请用 Mutex<T> 或 RwLock<T>,它们带真正的锁。把 RefCell 塞进线程是常见新手错误,编译器会拦,但你要知道为什么。
WarningRefCell 的
borrow_mut一旦在同一个作用域里已经有活跃的borrow,立刻 panic。有时这种冲突藏得很深——比如你在borrow()拿到的引用还活着时,调用了某个内部又会borrow_mut的方法。写 RefCell 包裹的类型时,方法要尽量短、不要在外面还拿着借用的情况下触发内部可变借用。
第三,别用 RefCell 掩盖设计问题。如果你的类型处处都要内部可变,可能说明它职责太多、状态太乱。先想想能不能把状态拆清楚、用正常的 &mut 传导,而不是一股脑包进 RefCell 图清净。
TipRefCell 和 Rc 是搭档不是替身:Rc 负责”多个人拥有同一份”,RefCell 负责”这份能被改”。二者解决的问题不同,组合起来才完整。单独用 RefCell 也能行,但它不改变所有权——它只是让”不可变绑定下的可变内部”成为可能。
下一章我们看一个危险陷阱:引用循环,以及它如何导致内存泄漏和如何防范。