Rc<T> 如何打破单一所有权?
解读
Rust 编译期的 Borrow Checker 默认执行“一个值只能有一个所有者”的硬规则,导致在单线程场景下若需多处共享同一份堆数据,直接 move 会触发“use after move”编译错误。Rc<T>(reference counted smart pointer)通过运行时引用计数机制,把“所有权”拆成“共享所有权”:每克隆一次只增加引用计数,不拷贝数据,也不转移所有权,从而在编译期仍满足所有权语义,在运行期允许多个 Rc<T> 同时指向同一块堆内存,实现“共享不可变,打破单一所有”。
知识点
- 单一所有权模型:Rust 的核心约束,值在任一时刻只有唯一所有者,离开作用域即 drop。
- Rc<T> 定义:
std::rc::Rc<T>,非线程安全,仅用于单线程;内部包含强引用计数与弱引用计数两个 usize。 - 共享所有权原理:
Rc::new(val)在堆上分配RcBox<T> { strong: 1, weak: 1, value: T },返回第一个所有者。- 每次
rc.clone()仅浅拷贝指针并原子级递增强引用计数,不触发深拷贝,成本 O(1)。 - 当强引用归零时 drop 数据;当强+弱均归零时释放堆内存,无需 GC 即可自动回收。
- 不可变约束:
Rc<T>不提供内部可变性;若需修改,需配合RefCell<T>形成单线程内部可变性模式Rc<RefCell<T>>。 - 循环引用风险:
Rc<T>相互克隆会形成引用计数循环,导致内存泄漏;可用Weak<T>降级打破循环。 - 与 Arc<T> 区别:
Arc<T>使用原子操作保证线程安全,成本更高;Rc<T>仅用于单线程高性能场景。
答案
Rc<T> 通过堆上引用计数把“唯一所有权”转为“共享所有权”:
- 调用
Rc::new(val)创建首所有者,堆上初始化引用计数为 1; - 使用
rc.clone()仅复制指针并原子递增计数,不转移所有权,从而允许多个Rc<T>并存; - 当最后一个
Rc<T>离开作用域、强引用归零时自动 drop 数据并释放内存; - 全程编译期仍满足所有权规则(因为
clone后原句柄仍合法),运行期实现共享只读访问,以此打破 Rust 的单一所有权限制。
注意:Rc<T>不可跨线程,且默认不可变;若需写操作,需搭配RefCell<T>使用。
拓展思考
- 面试常追问“Rc<RefCell<T>> 为什么能同时打破所有权与可变借用规则?”
答:Rc解决共享所有权,RefCell把借用检查从编译期推迟到运行期,通过borrow_mut()返回RefMut<T>,运行时动态检查“单写或多读”规则,形成单线程内部可变性经典组合。 - 若场景升级为多线程,需把
Rc换成Arc,把RefCell换成Mutex/RwLock,即Arc<Mutex<T>>,此时引用计数与锁都是原子操作,保证线程安全。 - 实战排查内存泄漏时,可用
Weak<T>把父子关系中的回指指针降级为弱引用,打破Rc循环,使引用计数能顺利归零。