Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

rust 内部可变性

1. 从一个“不能编译“的场景出发

我们有一个用 Rc<T> 共享的数据,希望多个持有者都能读取甚至修改它:

#![allow(unused)]
fn main() {
// 期望的行为(伪代码):
let shared_data = Rc::new(100);   // 多个模块共享一个计数器

// 在模块 A 中:
*shared_data += 1;  // 想通过 Rc 修改数据 → 编译错误!

// 在模块 B 中:
*shared_data += 1;  // 也想修改 → 编译错误!
}

为什么 Rust 编译器不允许这样做?用所有权和借用规则来分析:

  • Rc<T> 允许多个所有者共享数据,但它只提供不可变访问&T)。
  • 要修改数据,你需要可变引用&mut T),但借用规则规定:同一时刻只能有一个可变引用
  • 然而 Rc 的场景中,多个持有者同时存在——编译器无法在编译期确定“此刻是否只有一个持有者想修改数据“。于是编译器拒绝了所有修改操作。

这就带来了一个困境:逻辑上“多个持有者轮流修改同一份数据“是完全合理的需求(比如计数器、缓存、共享配置),但借用规则在编译期无法区分“轮流修改“和“同时修改“,于是一刀切地禁止了。

如果 Rust 只有编译期借用检查,代码的灵活性就到此为止了。为此,Rust 引入了内部可变性(Interior Mutability)

2. 什么是内部可变性?

内部可变性是一种设计模式:允许你通过不可变引用&self / &T)来修改内部数据。

这听起来像是在“破坏规则“,但 Rust 的策略不是“开个后门就完事了“,而是:把借用规则的检查从编译期推迟到运行时

编译期借用检查(默认):
    &T  → 一定不会修改数据 ← 编译器静态保证

内部可变性(推迟到运行时):
    &T  → 可能修改内部数据 ← 运行时检查借用规则
          如果违规 → panic!(而不是未定义行为)

设计理念:编译器不是万能的——有些正确的代码模式在编译期无法被证明安全。与其禁止这些模式,不如提供一种受控的方式,让程序员在运行时承担检查责任。这很像是数据库中的事务隔离级别:编译期检查 = 严格模式,零误判但有局限;运行时检查 = 宽松模式,能覆盖更多场景,但违规会 panic。

Rust 标准库提供了两种单线程内部可变性类型:

类型适用场景检查时机性能
Cell<T>整体读写/替换,尤其适合 Copy 类型(整数、bool 等)无运行时借用检查接近普通读写成本
RefCell<T>任意类型运行时检查借用规则轻微运行时开销

3. Cell<T> —— 整体读取和替换

3.1 功能定位

Cell<T> 可以存放任意 T,但它最常用于实现了 Copy trait 的类型(如 i32boolchar、指针等)。它的核心思路是:不提供内部数据的引用,只允许整体读取(get,要求 T: Copy)和整体替换(set / replace / take

因为不暴露 &T&mut T,所以不存在运行时借用冲突:Copy 类型可以通过 get() 读出拷贝,非 Copy 类型则通常用 replace() / take() 整体搬出或替换。Cell<T> 本身仍是单线程内部可变性工具,不实现 Sync

3.2 内部结构

Cell<T> {
    value: UnsafeCell<T>,   // 底层使用 UnsafeCell 绕过编译期借用检查
}

Cell<T> 的大小等于 T 的大小,直接内联存储数据(无额外堆分配)。

3.3 核心接口

创建 CellCell::new

功能:创建一个新的 Cell,用 value 初始化内部数据。

接口签名

#![allow(unused)]
fn main() {
pub const fn new(value: T) -> Cell<T>
}
  • 参数value — 初始值。
  • 返回值Cell<T> 实例。

简单调用示例

#![allow(unused)]
fn main() {
use std::cell::Cell;
let c = Cell::new(42);
}

读取值 — Cell::get

功能:返回内部值的拷贝。要求 T 实现了 Copy trait。

接口签名

#![allow(unused)]
fn main() {
pub fn get(&self) -> T
where
    T: Copy,
}
  • 参数&self(不可变引用即可调用)。
  • 返回值T — 内部值的拷贝。

简单调用示例

#![allow(unused)]
fn main() {
let c = Cell::new(10);
let val = c.get();   // val = 10
println!("{}", c.get());  // 可以多次调用
}

修改值 — Cell::set

功能:用新值整体替换内部数据。旧值被丢弃。

接口签名

#![allow(unused)]
fn main() {
pub fn set(&self, val: T)
}
  • 参数&self(不可变引用)+ val — 新值。
  • 返回值:无。

简单调用示例

#![allow(unused)]
fn main() {
let c = Cell::new(10);
c.set(20);           // 内部值变为 20
c.set(c.get() + 1);  // 读-改-写:内部值变为 21
}

替换并取出旧值 — Cell::replace

功能:用新值替换内部数据,并返回旧值。

接口签名

#![allow(unused)]
fn main() {
pub fn replace(&self, val: T) -> T
}
  • 参数&self + val — 新值。
  • 返回值T — 被替换出来的旧值。

取出值 — Cell::take

功能:取出内部值,并用 T::default() 替换。要求 T: Default

接口签名

#![allow(unused)]
fn main() {
pub fn take(&self) -> T
where
    T: Default,
}

简单调用示例

#![allow(unused)]
fn main() {
let c = Cell::new(String::from("hello"));
// 注意:String 不是 Copy,但可以用 replace/take
let old = c.take();        // old = "hello", 内部值变为 ""
let old2 = c.replace(String::from("world"));  // old2 = "", 内部值变为 "world"
}

3.4 典型示例:不可变计数器

use std::cell::Cell;

struct Counter {
    count: Cell<i32>,   // 不是 mut count: i32
}

impl Counter {
    fn new() -> Self {
        Counter { count: Cell::new(0) }
    }

    fn increment(&self) {              // &self, 不是 &mut self!
        let current = self.count.get();
        self.count.set(current + 1);
    }

    fn value(&self) -> i32 {
        self.count.get()
    }
}

fn main() {
    let counter = Counter::new();      // 不需要 mut
    counter.increment();                // 通过 &self 修改了内部状态
    counter.increment();
    println!("Count: {}", counter.value()); // 2
}

关键观察counter 不需要声明为 mutincrement 方法的签名是 &self 而非 &mut self,但它成功修改了内部计数器的值。这就是“内部可变性“的含义——数据在“不可变的外壳“下拥有“可变的内核“。


4. RefCell<T> —— 运行时借用检查

4.1 功能定位

RefCell<T> 适用于任意类型。与 Cell<T> 不同,RefCell 允许你获取内部数据的引用&T&mut T),但借用规则在运行时检查:

  • borrow() → 返回 Ref<T>(不可变借用),允许多个同时存在
  • borrow_mut() → 返回 RefMut<T>(可变借用),同一时刻只能有一个
  • 如果违反规则(如在已有不可变借用时调用 borrow_mut()),运行时 panic

4.2 内部结构

RefCell<T> {
    borrow_flag: Cell<isize>,   // 运行时借用计数器(0=无借用,>0=不可变借用数,-1=有一个可变借用)
    value: UnsafeCell<T>,       // 内部数据
}

RefCell<T> 的大小等于 T 的大小 + 一个 isize(借用标志位),数据直接内联存储。

4.3 核心接口

创建 RefCellRefCell::new

功能:创建一个新的 RefCell,用 value 初始化内部数据。

接口签名

#![allow(unused)]
fn main() {
pub const fn new(value: T) -> RefCell<T>
}
  • 参数value — 初始值。
  • 返回值RefCell<T> 实例。

简单调用示例

#![allow(unused)]
fn main() {
use std::cell::RefCell;
let rc = RefCell::new(vec![1, 2, 3]);
}

获取不可变借用 — RefCell::borrow

功能:获取内部数据的不可变引用 Ref<T>(它实现了 Deref<Target = T>,可以直接当 &T 用)。运行时检查:如果当前已存在可变借用,则 panic。允许多个不可变借用同时存在。

接口签名

#![allow(unused)]
fn main() {
pub fn borrow(&self) -> Ref<'_, T>
}
  • 参数&self
  • 返回值Ref<'_, T> — 一个包装了不可变借用的守卫,离开作用域时自动归还借用。

简单调用示例

#![allow(unused)]
fn main() {
let rc = RefCell::new(42);
let r1 = rc.borrow();
let r2 = rc.borrow();   // 多个不可变借用 OK
println!("{} {}", *r1, *r2);
}

获取可变借用 — RefCell::borrow_mut

功能:获取内部数据的可变引用 RefMut<T>(实现了 DerefMut)。运行时检查:如果当前已存在任何借用(不可变或可变),则 panic

接口签名

#![allow(unused)]
fn main() {
pub fn borrow_mut(&self) -> RefMut<'_, T>
}
  • 参数&self
  • 返回值RefMut<'_, T> — 一个包装了可变借用的守卫,离开作用域时自动归还借用。

简单调用示例

#![allow(unused)]
fn main() {
let rc = RefCell::new(42);
let mut r = rc.borrow_mut();
*r += 1;
println!("{}", *r);  // 43
// r 离开作用域,归还借用
}

安全的不可变借用 — RefCell::try_borrow

功能:和 borrow() 一样,但失败时不 panic,而是返回 Err。适用于生产环境中不确定借用状态的场景。

接口签名

#![allow(unused)]
fn main() {
pub fn try_borrow(&self) -> Result<Ref<'_, T>, BorrowError>
}
  • 返回值Ok(Ref<T>) 表示借用成功;Err(BorrowError) 表示当前不满足借用规则。

安全可变借用 — RefCell::try_borrow_mut

功能:和 borrow_mut() 一样,但失败时返回 Err 而非 panic。

接口签名

#![allow(unused)]
fn main() {
pub fn try_borrow_mut(&self) -> Result<RefMut<'_, T>, BorrowMutError>
}

简单调用示例

#![allow(unused)]
fn main() {
let rc = RefCell::new(42);
let r = rc.borrow();

// 尝试获取可变借用——此时不可变借用还在,应该失败
match rc.try_borrow_mut() {
    Ok(_) => println!("获取成功"),
    Err(_) => println!("已经存在不可变借用,无法获取可变借用"),
}
}

4.4 运行时借用规则演示

use std::cell::RefCell;

fn main() {
    let data = RefCell::new(42);

    // ✅ 多个不可变借用可以共存
    let b1 = data.borrow();
    let b2 = data.borrow();
    println!("{} {}", b1, b2);

    // ❌ 不可变借用未释放时尝试可变借用 → panic!
    // let b3 = data.borrow_mut();  // 运行时 panic: already borrowed

    // ✅ 正确做法:先释放不可变借用
    drop(b1);
    drop(b2);
    let mut b3 = data.borrow_mut();  // OK
    *b3 += 1;
    println!("{}", b3);
}

4.5 典型示例:带缓存的延迟计算

#![allow(unused)]
fn main() {
use std::cell::RefCell;

struct DataCache {
    computed: RefCell<Vec<i32>>,   // 缓存:可能被延迟填充
    raw_data: Vec<i32>,            // 原始数据:不可变
}

impl DataCache {
    fn new(data: Vec<i32>) -> Self {
        DataCache {
            computed: RefCell::new(Vec::new()),
            raw_data: data,
        }
    }

    fn get_computed(&self) -> Vec<i32> {     // &self, 不是 &mut self!
        let mut cache = self.computed.borrow_mut();

        if cache.is_empty() {
            // 首次访问,执行计算并填充缓存
            for &val in &self.raw_data {
                cache.push(val * 2);
            }
        }

        // 释放可变借用,切换为不可变借用以返回结果
        drop(cache);
        self.computed.borrow().clone()
    }
}
}

关键观察get_computed 的签名是 &self,外部调用者以为自己只是在“读取“数据。但在方法内部,首次调用时悄悄地修改了 computed 缓存。后续调用发现缓存已有数据,跳过计算直接返回。整个过程中,调用者完全不需要 &mut self——这就是内部可变性在“惰性初始化“场景中的经典应用。


5. 内部可变性不止于 Cell/RefCell

5.1 Rust 的 Mutex<T>RwLock<T> 也是内部可变性

很多从 Java、C++ 过来的开发者习惯这样的模式:

// Java:锁和数据是分离的
class SharedData {
    private int value;
    private final Lock lock = new ReentrantLock();

    void increment() {
        lock.lock();
        try { value++; } finally { lock.unlock(); }
    }
}

在这个模式中,**数据(value锁(lock)**是独立的对象。锁只是“门卫“,数据本身没有任何保护——如果你忘了加锁就访问数据,编译器和运行时都不会阻止你。

Rust 的 Mutex<T>RwLock<T> 采用了完全不同的设计:

#![allow(unused)]
fn main() {
use std::sync::Mutex;

let data = Mutex::new(42);     // 数据被"包裹"在 Mutex 内部
let mut guard = data.lock().unwrap();  // lock() 返回一个智能指针
*guard += 1;                    // 通过守卫访问/修改数据
// guard 离开作用域 → 自动解锁
}

核心差异

  • 数据和锁是一体的Mutex<T> 包裹了数据 T。你无法绕过锁直接访问数据——不调用 lock() 就看不到里面的值。
  • lock() 返回的是一个智能指针守卫MutexGuard<T>),它实现了 DerefDerefMut,让你可以像使用普通引用一样读写内部数据。守卫离开作用域时自动解锁——永远不会忘记 unlock()
  • Mutex<T> 本身提供内部可变性:你可以通过 &Mutex<T>(不可变引用)来修改内部数据。lock() 方法的签名是 &self,不是 &mut self

这和 RefCell<T> 的设计理念一脉相承:

Cell<T>      → 单线程,整体读写,无运行时借用检查
RefCell<T>   → 单线程,运行时借用检查
Mutex<T>     → 多线程,通过操作系统锁保证独占访问
RwLock<T>    → 多线程,读写锁(多读单写)

它们都实现了同一个模式:在“不可变“的外壳下提供“可变“的内部。区别只在于保证安全的方式不同——RefCell 用运行时计数器,Mutex 用操作系统锁,RwLock 用读写锁。

5.2 Rust 的设计理念

Rust 这种“用容器包裹数据、通过守卫访问“的设计,体现了一条核心哲学:

把安全约束编码为类型,让编译器强制执行。数据的所有权和访问权限,不应该依赖于程序员的记忆或编码规范,而应该是类型系统的一部分。

  • 传统语言:锁和数据分离 → 安全靠“记得加锁“
  • Rust:Mutex<T> 包裹数据 → 不加锁就拿不到数据,安全靠类型系统保证

同样的哲学也贯穿在 Cell/RefCell 中:你想修改不可变引用背后的数据?用 RefCell<T> 包裹它,借用规则从编译期推迟到运行时,但依然被检查——违规即 panic,不会静默地产生未定义行为。


6. 实验代码示例

配套代码位于 src/study/interior_mutability.rs

6.1 Cell<T> — 不可变结构体的内部计数

#![allow(unused)]
fn main() {
use std::cell::Cell;

struct Counter {
    count: Cell<i32>,
}

impl Counter {
    fn new() -> Self {
        Self { count: Cell::new(0) }
    }

    fn increment(&self) {
        let current = self.count.get();
        self.count.set(current + 1);
    }

    fn value(&self) -> i32 {
        self.count.get()
    }
}

fn cell_demo() {
    let counter = Counter::new();    // 不需要 mut
    counter.increment();             // &self 也能修改内部值
    counter.increment();
    println!("Cell 计数器结果: {}", counter.value());  // 2
}
}
  • 实验:定义 Counter 结构体,内部用 Cell<i32> 而非 i32,所有方法签名均为 &self。创建不可变的 counter 实例,调用 increment() 多次。
  • 现象:编译通过,输出 Cell 计数器结果: 2counter 未声明为 mut,但值确实被修改了。
  • 分析Cell::get() 返回值的拷贝,Cell::set() 整体替换内部值——这两个操作都不需要引用内部数据,因此不存在引用冲突。Cell 不提供 &T&mut T,只提供值语义的读写,所以无需运行时借用检查,成本接近普通读写。
  • 结论Cell<T> 适合 Copy 类型的内部可变性场景(计数器、标志位等)。使用体验接近“正常的可变变量“,但允许你通过 &self 修改它。

6.2 RefCell<T> — 带缓存的延迟计算

#![allow(unused)]
fn main() {
use std::cell::RefCell;

struct DataCache {
    computed: RefCell<Vec<i32>>,
    raw_data: Vec<i32>,
}

impl DataCache {
    fn new(data: Vec<i32>) -> Self {
        Self { computed: RefCell::new(Vec::new()), raw_data: data }
    }

    fn get_computed(&self) -> Vec<i32> {
        let mut cache = self.computed.borrow_mut();

        if cache.is_empty() {
            for &val in &self.raw_data {
                cache.push(val * 2);
            }
        }

        drop(cache);  // 释放可变借用
        self.computed.borrow().clone()  // 获取不可变借用,克隆返回
    }
}

fn refcell_cache_demo() {
    let cache = DataCache::new(vec![1, 2, 3]);
    println!("第一次计算缓存: {:?}", cache.get_computed()); // [2, 4, 6]
    println!("第二次直接复用缓存: {:?}", cache.get_computed()); // [2, 4, 6]
}
}
  • 实验DataCache 内部用 RefCell<Vec<i32>> 缓存计算结果。首次调用 get_computed()&self)时填充缓存,后续调用直接返回缓存内容。
  • 现象:两次调用输出相同结果 [2, 4, 6]。首次调用触发了计算(在 &self 方法内修改了 computed),第二次调用跳过计算直接复用缓存。调用者始终只需要 &self,完全感知不到内部的修改行为。
  • 分析borrow_mut() 在运行时检查——首次调用时没有其他借用,成功获取可变引用并填充缓存。drop(cache) 显式释放可变借用后,borrow() 才能成功。如果忘记 drop(cache),下一行的 borrow() 会 panic(因为可变借用还未归还)。这种局部的“先写后读“模式在编译期无法被证明安全,但 RefCell 让你在运行时安全地执行它。
  • 结论RefCell<T> 允许任意类型享受内部可变性。运行时检查确保了“违规即 panic“,代价是轻微的运行开销和需要程序员注意借用归还的时机。

6.3 RefCell 的运行时借用规则验证

#![allow(unused)]
fn main() {
use std::cell::RefCell;

fn refcell_borrow_rule_demo() {
    let data = RefCell::new(42);

    let b1 = data.borrow();
    let b2 = data.borrow();
    println!("多个不可变借用可以共存: {} {}", b1, b2);

    // 不可变借用还在 → try_borrow_mut 返回 Err,而不是 panic
    if data.try_borrow_mut().is_err() {
        println!("已经存在不可变借用,无法获取可变借用");
    }

    drop(b1);
    drop(b2);

    let mut b3 = data.borrow_mut();
    *b3 += 1;
    println!("不可变借用释放后,可变借用成功: {}", b3);
}
}
  • 实验:先获取多个不可变借用,用 try_borrow_mut() 尝试获取可变借用,释放不可变借用后再获取可变借用并修改。
  • 现象:不可变借用共存正常;try_borrow_mut() 返回 Err 而非 panic;释放后 borrow_mut() 成功,值从 42 变为 43。
  • 分析:这个实验直接验证了 RefCell 的运行时借用规则——和编译期借用规则完全一致(多读或单写),区别只是违规时编译期报错 vs 运行期 panic(或 try_* 返回 Err)。try_borrow_mut() 是生产环境推荐的做法——因为 panic 会终止线程,而 Err 可以被优雅处理。
  • 结论RefCell 的运行时借用规则和编译期规则完全对应。try_borrow / try_borrow_mut 提供了不 panic 的安全替代方案。

7. 总结:内部可变性的选择指南

场景使用原因
内部数据是 Copy 类型,只需整体读写Cell<T>无运行时借用检查,成本接近普通读写
内部数据是任意类型,需要获取引用RefCell<T>运行时借用检查,灵活
想避免 panic,优雅处理借用冲突RefCell + try_borrow / try_borrow_mut失败返回 Err
多线程共享可变数据Mutex<T> / RwLock<T>同样基于内部可变性模式

核心理解:内部可变性不是 Rust 的“妥协“,而是一种精心的设计——把无法在编译期证明的安全规则推迟到运行时执行,在灵活性和安全性之间找到了平衡点。CellRefCellMutexRwLock 共享同一个设计哲学:数据被容器包裹,访问通过守卫,守卫离开时自动归还权限