bigywater.tech
Rust · 32

RefCell<T> 运行时借用检查

RefCell 是「内部可变性」的代表:借用规则不变,只是检查从编译期搬到了运行期。这篇验证它能做什么、违反规则时会怎样、与编译期借用的对照,以及典型的 Rc> 组合。

panic
运行期违反借用规则:RefCell already borrowed(退出码 101)
16 字节
RefCell 的大小(u64 是 8)
Rc>
共享所有权 + 可修改的惯用组合

内部可变性

实测 · 本机执行输出
  源码:
      1 | use std::cell::RefCell;
      2 | fn main() {
      3 |     let c = RefCell::new(vec![1, 2, 3]);       // 注意:c 不是 mut
      4 |     c.borrow_mut().push(4);                     // 通过共享引用修改
      5 |     println!("{:?},长度 {}", c.borrow(), c.borrow().len());
      6 |     {
      7 |         let r1 = c.borrow(); let r2 = c.borrow();      // 多个不可变借用:可以
      8 |         println!("同时两个 borrow():{} {}", r1.len(), r2.len());
      9 |     }
     10 | }
  编译:成功
  运行(退出码 0):
    [1, 2, 3, 4],长度 4
    同时两个 borrow():4 4

运行期检查

实测 · 本机执行输出
  编译:成功
  运行(退出码 101):
    持有一个 borrow() 时再 borrow_mut() ...
    
    thread 'main' (6994266) panicked at <实验目录>/src/u32b.rs:6:15:
    RefCell already borrowed
    note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
  源码:
      1 | use std::cell::RefCell;
      2 | fn main() {
      3 |     let c = RefCell::new(5);
      4 |     let _w1 = c.borrow_mut();
      5 |     println!("try_borrow_mut() 在已有可变借用时:{:?}", c.try_borrow_mut().is_err());
      6 |     println!("try_borrow()     在已有可变借用时:{:?}", c.try_borrow().is_err());
      7 | }
  编译:成功
  运行(退出码 0):
    try_borrow_mut() 在已有可变借用时:true
    try_borrow()     在已有可变借用时:true

与编译期对照

实测 · 本机执行输出
  源码:
      1 | fn main() {
      2 |     let mut x = 5;
      3 |     let r = &x;
      4 |     let w = &mut x;
      5 |     println!("{} {}", r, w);
      6 | }
  编译:失败
    error[E0502]: cannot borrow `x` as mutable because it is also borrowed as immutable
     --> <实验目录>/src/u32d.rs:4:13
      |
    3 |     let r = &x;
      |             -- immutable borrow occurs here
    4 |     let w = &mut x;
      |             ^^^^^^ mutable borrow occurs here
    5 |     println!("{} {}", r, w);
      |                       - immutable borrow later used here
    
    error: aborting due to 1 previous error
    
    For more information about this error, try `rustc --explain E0502`.

编译期借用规则见不可变借用与可变借用。

组合与成本

实测 · 本机执行输出
  源码:
      1 | use std::cell::RefCell;
      2 | use std::rc::Rc;
      3 | fn main() {
      4 |     let shared = Rc::new(RefCell::new(Vec::<i32>::new()));
      5 |     let a = Rc::clone(&shared); let b = Rc::clone(&shared);
      6 |     a.borrow_mut().push(1);
      7 |     b.borrow_mut().push(2);
      8 |     println!("两个所有者各自 push 一次,共享的 Vec = {:?}", shared.borrow());
      9 | }
  编译:成功
  运行(退出码 0):
    两个所有者各自 push 一次,共享的 Vec = [1, 2]
  编译:成功
  运行(退出码 0):
    size_of::<u64>() = 8,size_of::<RefCell<u64>>() = 16(借用标志是一个 isize,占 8 字节)
    size_of::<u8>() = 1,size_of::<RefCell<u8>>() = 16

不借用而是直接替换值的轻量版本见Cell<T>;线程安全版本见Mutex<T>。

分步演示

RefCell
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / rustc 1.98.1(brew)/ cargo 1.98.1;单个文件用 rustc 直接编译,需要依赖时用 cargo(经代理从 crates.io 下载);计时用 std::time::Instant,数字每次运行会有小幅抖动。我尝试用循环量 RefCell 的耗时开销,结果被编译器优化干扰、不可信,所以没有列出
    • 没有验证:RefCell 的运行期开销、Ref::map、借用标志的具体布局、在多线程里 RefCell 不是 Sync 的报错(见 Sync 一篇)
    京ICP备2026067502号-1京公网安备11010502063487号