Rust Rc/RefCell 与内部可变性
更新时间:2026-09-18。当你需要「多个变量共享同一份数据」且「共享中还要修改」,单线程答案就是
Rc<RefCell<T>>。所有权基础见 所有权与借用规则,跨线程版本见 Send/Sync。
一、Rc:单线程多所有者
rust
use std::rc::Rc;
let a = Rc::new(vec![1, 2, 3]);
let b = Rc::clone(&a); // 只把引用计数 +1,不深拷贝 Vec
println!("{}", Rc::strong_count(&a)); // 2Rc用非原子引用计数,开销小但不是Send,只能在单线程内共享。Rc<T>给你的是不可变访问;它本身不允许你改内部的T(否则违反多读一写)。
二、RefCell:内部可变性
RefCell<T> 把借用规则的检查从编译期移到运行期:
rust
use std::cell::RefCell;
let c = RefCell::new(0);
*c.borrow_mut() += 1; // 可变借用
{
let r1 = c.borrow(); // 不可变借用
let _r2 = c.borrow(); // 多个不可变 OK
}
// let mut w = c.borrow_mut(); // 若上面的 r 还活着,这里在运行时 panic!
println!("{}", c.borrow()); // 1borrow()/borrow_mut() 返回的守卫(Ref/RefMut)离开作用域时归还借用;同一时刻若违反「多读或一写」,程序在运行时 panic: already mutably borrowed。
三、组合:Rc<RefCell<T>>
要「多处共享 + 可变」,二者组合:
rust
use std::{rc::Rc, cell::RefCell};
let shared = Rc::new(RefCell::new(String::from("a")));
let s2 = Rc::clone(&shared);
shared.borrow_mut().push('b');
println!("{}", s2.borrow()); // ab:两个所有者看到同一份这相当于单线程版的「可变共享指针」。跨线程对应物是 Arc<Mutex<T>>:原子计数 + 锁,运行时保证互斥而非 panic。
四、Cell 与弱引用
Cell<T>:要求T: Copy,通过get()复制、set()整体替换来绕过不可变,适合小的 Copy 类型(计数器、开关),无运行时借用守卫。RefCell适合非 Copy 的复杂类型。- 循环引用:
Rc相互强引用会让计数永不归零、内存泄漏。树/图中从子指向父、或观察者回调,用Weak<T>(Rc::downgrade,不增强计数,upgrade()返回Option<Rc<T>>)。
五、常见坑
- 跨线程用 Rc/RefCell:编译就过不了(不是
Send/Sync),换Arc/Mutex。 - 守卫没及时释放又借:
borrow()返回值一直活到作用域结束,期间borrow_mutpanic;用小作用域或先drop(guard)。 - 把 Rc::clone 当深拷贝:共享同一数据,一处改处处可见。
- 强引用成环泄漏:用
Weak打断环。 - 运行时 panic 不如编译错误:能用普通引用/所有权表达就别引入 RefCell,它是「确实需要内部可变」时的受控逃生口。
总结
Rc 用非原子引用计数让单线程中一个值拥有多个共享者、Rc::clone 只增计数不深拷贝但只给不可变访问且不能跨线程;RefCell 通过 borrow/borrow_mut 守卫把多读一写检查推迟到运行时,从而在共享不可变外壳下实现内部可变性,违规会 panic 而非编译失败;二者组合 Rc<RefCell<T>> 就是单线程可变共享指针,跨线程对应 Arc<Mutex<T>>,Copy 小值用 Cell,并且要用 Weak 打断 Rc 强引用环避免泄漏,能用普通借用解决的问题就不要引入这套运行时机制。