Rc<T> 如何打破单一所有权?

解读

Rust 编译期的 Borrow Checker 默认执行“一个值只能有一个所有者”的硬规则,导致在单线程场景下若需多处共享同一份堆数据,直接 move 会触发“use after move”编译错误。Rc<T>(reference counted smart pointer)通过运行时引用计数机制,把“所有权”拆成“共享所有权”:每克隆一次只增加引用计数,不拷贝数据,也不转移所有权,从而在编译期仍满足所有权语义在运行期允许多个 Rc<T> 同时指向同一块堆内存,实现“共享不可变,打破单一所有”。

知识点

  1. 单一所有权模型:Rust 的核心约束,值在任一时刻只有唯一所有者,离开作用域即 drop。
  2. Rc<T> 定义std::rc::Rc<T>非线程安全,仅用于单线程;内部包含强引用计数弱引用计数两个 usize。
  3. 共享所有权原理
    • Rc::new(val) 在堆上分配 RcBox<T> { strong: 1, weak: 1, value: T },返回第一个所有者。
    • 每次 rc.clone()浅拷贝指针并原子级递增强引用计数,不触发深拷贝,成本 O(1)。
    • 强引用归零时 drop 数据;当强+弱均归零时释放堆内存,无需 GC 即可自动回收。
  4. 不可变约束Rc<T> 不提供内部可变性;若需修改,需配合 RefCell<T> 形成单线程内部可变性模式 Rc<RefCell<T>>
  5. 循环引用风险Rc<T> 相互克隆会形成引用计数循环,导致内存泄漏;可用 Weak<T> 降级打破循环。
  6. 与 Arc<T> 区别Arc<T> 使用原子操作保证线程安全,成本更高;Rc<T> 仅用于单线程高性能场景

答案

Rc<T> 通过堆上引用计数把“唯一所有权”转为“共享所有权”:

  1. 调用 Rc::new(val) 创建首所有者,堆上初始化引用计数为 1;
  2. 使用 rc.clone() 仅复制指针并原子递增计数,不转移所有权,从而允许多个 Rc<T> 并存;
  3. 当最后一个 Rc<T> 离开作用域、强引用归零时自动 drop 数据并释放内存;
  4. 全程编译期仍满足所有权规则(因为 clone 后原句柄仍合法),运行期实现共享只读访问,以此打破 Rust 的单一所有权限制。
    注意:Rc<T> 不可跨线程,且默认不可变;若需写操作,需搭配 RefCell<T> 使用。

拓展思考

  1. 面试常追问“Rc<RefCell<T>> 为什么能同时打破所有权与可变借用规则?
    答:Rc 解决共享所有权,RefCell 把借用检查从编译期推迟到运行期,通过 borrow_mut() 返回 RefMut<T>,运行时动态检查“单写或多读”规则,形成单线程内部可变性经典组合。
  2. 若场景升级为多线程,需把 Rc 换成 Arc,把 RefCell 换成 Mutex/RwLock,即 Arc<Mutex<T>>,此时引用计数与锁都是原子操作,保证线程安全。
  3. 实战排查内存泄漏时,可用 Weak<T> 把父子关系中的回指指针降级为弱引用,打破 Rc 循环,使引用计数能顺利归零。