📝 文章摘要

Rust 的核心价值主张是“编译时内存安全”,这由所有权和借用检查器(Borrow Checker)强制执行。然而,有时这些规则过于严格。本文将深入探讨“内部可变性”(Interior Mutability)模式,这是一种在 Rust 中安全地“绕过”编译时规则的方式。我们将剖析 UnsafeCell 作为所有内部可变性类型的构建基石,并详细对比 Cell<T>(用于 Copy 类型)和 RefCell<T>(用于运行时借用检查)的实现原理、性能开销和适用场景。


一、背景介绍

1.1 借用检查器的限制

Rust 的借用规则(“一个可变引用 &mut T”或“多个不可变引用 &T”,但不能同时存在)在编译时保证了数据竞争的安全。

// ❌ 编译错误
fn modify_while_borrowed(data: &String) {
    let mut_ref = &mut data; // 错误:不能从 &String 获取 &mut String
    mut_ref.push_str("!");
}

但在某些设计模式中(如图形结构、缓存、观察者模式),我们确实需要在一个对象看似“不可变”(`&self时,修改其内部的某些字段。这就是内部可变性的用武之地。

1.2 内部可变性模式

内部可变性(Interior Mutability)是一种设计模式,它允许您在拥有不可变引用(&T)时仍能修改 `T 内部的数据。

在这里插入图片描述


二、原理详解

2.1 一切的根源:UnsafeCell<T>

UnsafeCell<T> 是 Rust 内部可变性机制的唯一原语。

// std::cell::UnsafeCell
pub struct UnsafeCell<T> {
    value: T,
}

impl<T> UnsafeCell<T> {
    pub fn get(&self) -> *mut T {
        // 关键:它允许从 &self (不可变引用)
        // 获取 *mut T (原始可变指针)
        self as *const Self as *mut T
    }
}

Rust 编译器对 UnsafeCell<T> 有一个特殊的规则:**如果一个类型 T 包含 UnsafeCell<U>, T 就“不认为” &T 是真正不可变的**。CellRefCellMutex 内部都封装了 UnsafeCell

2.2 Cell<T>:零成本的 Copy 类型可变性

`Cell<T>用于实现了 Copy Trait 的类型(如 i32u64bool)。

**`Cell<T>(概念上)**:

use std::cell::UnsafeCell;

pub struct Cell<T: Copy> {
    value: UnsafeCell<T>,
}

impl<T: Copy> Cell<T> {
    pub fn new(value: T) -> Self {
        Cell { value: UnsafeCell::new(value) }
    }
    
    // 核心:set() 
    pub fn set(&self, value: T) {
        // &self 是不可变引用
        // 1. 获取 *mut T
        let ptr = self.value.get();
        // 2. unsafe: 写入数据
        unsafe { *ptr = value; }
    }
    
    // 核心:get()
    pub fn get(&self) -> T {
        // 1. 获取 *mut T (即使是读取也用 *mut)
        let ptr = self.value.get();
        // 2. unsafe: 读取数据 (因为是 Copy,直接复制)
        unsafe { *ptr }
    }
}

关键点Cell<T> *从* 给出 &T 或 &mut T。它只允许你 set(替换)或 get(复制)整个值。因为它只适用于 Copy 类型,所以没有撕裂读写(Torn Read)的风险,并且是零运行时开销的(没有锁,没有计数器)。

2.3 RefCell<T>:运行时的借用检查

RefCell<T> 适用于非 Copy 类型(如 StringVec<T>)。它在运行时执行借用检查。

**`RefCell<T>(概念上)**:

use std::cell::{UnsafeCell, Cell};

// 借用状态
#[derive(Copy, Clone, PartialEq)]
enum BorrowState {
    Readable(usize), // 有 N 个读者
    Writable,        // 有 1 个写者
    Unused,          // 未被借用
}

pub struct RefCell<T> {
    value: UnsafeCell<T>,
    state: Cell<BorrowState>, // 使用 Cell 来修改状态
}

impl<T> RefCell<T> {
    pub fn new(value: T) -> Self {
        RefCell {
            value: UnsafeCell::new(value),
            state: Cell::new(BorrowState::Unused),
        }
    }

    // 获取不可变引用
    pub fn borrow(&self) -> Ref<T> {
        match self.state.get() {
            BorrowState::Unused => {
                self.state.set(BorrowState::Readable(1));
                Ok(Ref::new(self))
            }
            BorrowState::Readable(n) => {
                self.state.set(BorrowState::Readable(n + 1));
                Ok(Ref::new(self))
            }
            BorrowState::Writable => {
                panic!("RefCell: 已被可变借用"); // 运行时 Panic!
            }
        }.unwrap() // 简化
    }

    // 获取可变引用
    pub fn borrow_mut(&self) -> RefMut<T> {
        match self.state.get() {
            BorrowState::Unused => {
                self.state.set(BorrowState::Writable);
                Ok(RefMut::new(self))
            }
            BorrowState::Readable(_) | BorrowState::Writable => {
                panic!("RefCell: 已被借用"); // 运行时 Panic!
            }
        }.unwrap() // 简化
    }
}

// Ref 和 RefMut 是智能指针,在 Drop 时恢复 state

关键点RefCell<T> 将编译时借用检查推迟到了运行时。如果违反规则(如同时 borrow_mut 两次),程序会 panic


三、代码实战

3.1 实战:`CellT>` (配置管理)

use std::cell::Cell;

// 全局配置,我们希望通过 &self 更新
struct AppConfig {
    version: Cell<u32>,
    api_key: String, // 假设 API Key 不可变
}

impl AppConfig {
    // 只需要 &self
    fn update_version(&self, new_version: u32) {
        // 使用 .set() 修改
        self.version.set(new_version);
    }
    
    fn get_version(&self) -> u32 {
        // 使用 .get() 读取
        self.version.get()
    }
}

fn main() {
    let config = AppConfig {
        version: Cell::new(1),
        api_key: "key_123".to_string(),
    };
    
    println!("当前版本: {}", config.get_version());
    
    // 即使 config 是不可变的,我们也可以修改其内部的 Cell
    config.update_version(2);
    
    println!("更新后版本: {}", config.get_version());
}

3.2 实战:RefCell<T> (观察者模式)

RefCell 经常与 Rc(引用计数)结合使用,以实现多所有权的内部可变性。

use std::rc::{Rc, Weak};
use std::cell::RefCell;

// 被观察者(主题)
struct Subject<'a> {
    observers: Vec<Weak<RefCell<dyn Observer + 'a>>>,
    value: RefCell<i32>,
}

// 观察者 Trait
trait Observer {
    fn notify(&self, value: i32);
}

impl<'a> Subject<'a> {
    fn new() -> Self {
        Subject { observers: Vec::new(), value: RefCell::new(0) }
    }
    
    fn attach(&mut self, observer: Weak<RefCell<dyn Observer + 'a>>) {
        self.observers.push(observer);
    }
    
    fn set_value(&self, value: i32) {
        // 1. 使用 RefCell 修改内部值
        *self.value.borrow_mut() = value;
        
        // 2. 通知所有观察者
        for observer_weak in &self.observers {
            if let Some(observer_rc) = observer_weak.upgrade() {
                // 3. 观察者也使用 RefCell
                observer_rc.borrow().notify(value);
            }
        }
    }
}

// 具体的观察者
struct ConcreteObserver {
    name: String,
}
impl Observer for ConcreteObserver {
    fn notify(&self, value: i32) {
        println!("观察者 [{}]: 收到新值 {}", self.name, value);
    }
}

fn main() {
    // 使用 Rc<RefCell<T>> 模式
    let subject = Rc::new(RefCell::new(Subject::new()));
    
    let observer1 = Rc::new(RefCell::new(ConcreteObserver { name: "A".to_string() }));
    let observer2 = Rc::new(RefCell::new(ConcreteObserver { name: "B".to_string() }));
    
    // 注册观察者
    subject.borrow_mut().attach(Rc::downgrade(&observer1));
    subject.borrow_mut().attach(Rc::downgrade(&observer2));

    // 修改值,触发通知
    // 我们持有的是 &Subject (通过 RefCell),但可以调用 set_value
    subject.borrow().set_value(100);
}

3.3 实战:RefCell 的运行时 Panic

use std::cell::RefCell;

fn main() {
    let data = RefCell::new(String::from("hello"));

    // 第一次可变借用 (成功)
    let mut b1 = data.borrow_mut();
    b1.push_str(" world");
    
    println!("b1: {}", b1);

    // 第二次可变借用 (在 b1 释放之前)
    println!("尝试第二次可变借用...");
    
    // 触发 Panic!
    // thread 'main' panicked at 'RefCell: 已被可变借用'
    let b2 = data.borrow_mut(); 
    println!("b2: {}", b2);

    // 必须确保 b1 在 b2 之前被 drop
    // drop(b1);
    // let b2 = data.borrow_mut(); // 这才会成功
}

四、结果分析

4.1 性能与开销对比

类型 检查时机 线程安全 性能开销 失败后果
&mut T (标准) 编译时 零开销 编译失败
`Cell<T> 编译时 零开销 (仅限 Copy) N/A
RefCell<T> 运行时  (原子) Panic
Mutex<T> 运行时 **是  (系统调用) 阻塞/中毒
4.2 何时使用 unsafe

您不应该直接使用 UnsafeCell,除非您正在构建一个新的、安全的抽象(例如您自己的 MyMutex<T>)。Cell 和 `RefCell 已经提供了 99% 场景下的安全内部可变性封装。

使用 unsafe

use std::cell::UnsafeCell;

// 示例:实现一个简单的原子 bool (不推荐,请用 AtomicBool)
struct MyFlag {
    value: UnsafeCell<bool>,
}
unsafe impl Sync for MyFlag {} // 承诺线程安全

impl MyFlag {
    // 必须使用 unsafe 块
    unsafe fn set_true(&self) {
        // 必须自己保证原子性
        *self.value.get() = true; 
    }
}

五、总结与讨论

5.1 核心要点

  • 内部可变性:在 &T (不可变) 引用上修改内部数据的模式。
  • Unsafel<T>:是所有内部可变性类型的基石,它“关闭”了编译器的 \&T\ 不可变检查。
  • Cell<T>:用于 Copy 类型,通过 get()/set() 复制值,零运行时开销。
  • RefCell<T>:用于非 Copy 类型,通过 borrow()/borrow_mut() 在运行时检查借用规则,违规则 Panic
  • Rc<RefCell<T>>:是 Rust 中实现复杂图状数据结构或观察者模式的标准(单线程)方式。
5.2 讨论问题
  1. RefCell<T> 导致的运行时 panic 是否违背了 Rust“编译时安全”的承诺?
  2. Cell<T> 为什么被限制为 `Copy 类型?如果不限制会发生什么?
  3. Mutex<T> 和 RefCell<T> 有何异同?(提示:线程安全 vs 运行时检查)
  4. 在您的项目中,哪些场景迫使您使用了 RefCell

参考链接

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐