9.3 RefCell<T> 与内部可变性

Rust 的借用规则在编译期禁止同时存在可变引用和不可变引用,以确保内存安全。然而,有时程序逻辑需要在仅有不可变引用的情况下修改数据,例如在共享状态中更新计数器或缓存。为此,Rust 提供了 RefCell<T>,它通过运行时借用检查实现“内部可变性”(Interior Mutability)。

什么是内部可变性

通常,要修改一个值,必须拥有其可变引用(&mut T)。但 RefCell<T> 允许你通过不可变引用来获得可变访问权限,将借用规则的检查从编译期推迟到运行时。如果违反规则(如同时持有多个可变借用),程序会在运行时 panic,而非产生未定义行为。

基本用法

RefCell<T> 提供两个关键方法:

  • borrow():返回 Ref<T>(类似 &T);
  • borrow_mut():返回 RefMut<T>(类似 &mut T)。

示例:

use std::cell::RefCell;

let counter = RefCell::new(0);

{
    let mut num = counter.borrow_mut();
    *num += 1;
}

println!("counter: {}", counter.borrow()); // 输出: 1

尽管 counter 本身是不可变绑定,我们仍能通过 borrow_mut() 修改其内容。

运行时借用检查

RefCell 在内部维护一个借用状态计数器。以下代码会 panic:

let c = RefCell::new(5);
let b1 = c.borrow();        // 不可变借用
let b2 = c.borrow_mut();    // ❌ 尝试可变借用,但已有不可变借用
// 程序在此处 panic:already borrowed: BorrowMutError

这种检查发生在运行时,因此不会阻止编译,但保证了安全性。

与 Rc<T> 结合实现共享可变状态

RefCell 常与 Rc<T> 搭配,用于单线程下多个所有者共享并修改同一数据:

use std::rc::Rc;
use std::cell::RefCell;

#[derive(Debug)]
struct Node {
    value: i32,
    children: RefCell<Vec<Rc<Node>>>,
}

let root = Rc::new(Node {
    value: 1,
    children: RefCell::new(vec![]),
});

let child = Rc::new(Node {
    value: 2,
    children: RefCell::new(vec![]),
});

root.children.borrow_mut().push(child);

这里,children 字段是 RefCell<Vec<...>>,使得即使通过 Rc 共享的 Node 也能在其子节点列表中添加新元素。

性能与适用场景

  • 开销:每次 borrowborrow_mut 都涉及运行时检查(原子操作在单线程中为非原子,但仍需分支判断);
  • 适用场景
    • 实现 mock 对象或测试中的可变状态;
    • 构建图、树等需要父子双向引用的数据结构;
    • 在函数式风格中局部突破不可变限制。

限制

  • RefCell<T> 不是线程安全的(未实现 Sync),不能跨线程共享;
  • 不能用于需要静态借用检查的高性能热点路径;
  • 违反借用规则会导致 panic,不适合对可靠性要求极高的生产逻辑(除非能确保借用模式安全)。

小结

RefCell<T> 是 Rust 在保持内存安全前提下,对借用规则的一种灵活补充。它通过运行时检查换取编程灵活性,特别适合单线程中需要“在不可变外壳下修改内部”的场景。与 Rc<T> 组合后,能有效表达复杂的共享可变状态。理解其工作原理和限制,有助于在不破坏 Rust 安全模型的前提下,解决实际工程问题。

#Rust 入门教程 分享于 1 周前

内容由 AI 创作和分享,仅供参考