Rust unsafe 与内部可变性:UnsafeCell、、Cell与RefCell` 精讲
目录
2.1 一切的根源:`UnsafeCell UnsafeCell 是 Rust 内部可变性机制的唯一原语。它是一个 struct,只有一个方法:
📝 文章摘要
Rust 的核心价值主张是“编译时内存安全”,这由所有和借用检查器(Borrow Checker)强制执行。然而,有时这些规则过于严格。本文将深入探讨“内部可变性”(Interior Mutabilityy)模式,这是一种在 Rust 中安全地“绕过”编译时规则的方式。我们将剖析 UnsafeCell 作为所有内部可变性类型的的构建基石,并详细对比 Cell<T>(用于 Copy 类型)和 RefCell<T>(用于运行时借用检查)的的实现原理、性能开销和适用场景。
一、背景介绍
1.1 借用检查器的限制
Rust 的(“一个可变引用 &mut T”或“多个不可变引用 &T”,但不能同时存在)在编译时保证了数据竞争的安全。
// ❌ 编译错误
struct MyCounter {
count: u32,
}
impl MyCounter {
// 无法编译:`self` 是 `&self` (不可变)
// 但 `self.count` 需要可变变
fn increment(&self) {
// self.count += 1;
}
}
在某些设计模式中(如图结构、缓存、观察者模式),我们确实需要在一个对象看似“不可变”(&self)时,修改其内部的某些字段。这就是就是内部可变性的用武之地。
1.2 内部可变性模式
内部可变性(Interior Mutability)是一种设计,它允许您在拥有不可变引用(&T)时仍能修改 T 内部的数据。

二、原理详解
2.1 一切的根源:`UnsafeCell<T>UnsafeCell<T> 是 Rust 内部可变性机制的唯一原语。它是一个 struct,只有一个方法:
// std::cell::UnsafeCell
#[repr(transparent)]
pub struct UnsafeCell<T: ?Sized> {
valueue: T,
}
impl<T> UnsafeCell<T> {
pub fn get(&self) -> *mut T {
//键:它允许从 &self (不可变引用)
// 获取 *mut T (原始可变指针)
self as *const Selflf as *mut T
}
}
Rust 编译器对 UnsafeCell<T> 有一个特殊的规则:**如果一个类型包含UnsafeCell,T就“不认为”&T 是真正不可变的**。\CellRefCell、Mutex 内部都封装了 UnsafeCell。
2.2 Cell<T>:>:零成本的 Copy` 类型可变性
Cell<T> 适用于实现了 Copy Trait 的类型(如 `i32, u64, bool)。
Cell<T>(概念上):
use std::cell::afeCell;
pub struct Cell<T: Copy> {
value: UnsafeCell<T>,
}
impl<T: Copyy> 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` 类型(如 `String`, `Vec<T>`)。它在**运行时**执行借用检查。
**`RefCell<T>`(概念上)**:
```rust
use std::cell::{UnsafeCell, Cell};
// 借用状态
#[derive(Copy, Clone, PartialEq)]
enum BorrowState {
Readable(usize), // 有 N 个读者
Writable,le, // 有 1 个写者
Unused, // 未被借用
}
pub struct RefCell<T> {
value: UnafeCell<T>,
state: Cell<BorrowState>, // 使用 Cell 来修改状态
}
impl<T> RefCell<T> { {
pub fn new(value: T) -> Self {
RefCell {
value: UnsafeCell::new(value),
state:ell::new(BorrowState::Unused),
}
}
// 获取不可变引用
pub fn borrow(&self) ->-> Ref<T> { // Ref<T> 是一个 RAII 守卫
match self.state.get() {
BorrowState::Un => {
self.state.set(BorrowState::Readable(1));
// ... 返回 Ref 守卫 ...
}
BorrowState::Readable(n) => {
self.state.set(BorrowState::Readable(n + 1));
// ... 返回 Reff 守卫 ...
}
BorrowState::Writable => {
panic!("RefCell: 已被可变借用"); // 运行时 Panic!!
}
}
// ... (省略 Ref<T> 的实现) ...
unimplemented!()
}
// 获取可变引用
pub fn borrow_mut(&self) -> RefMut<T> { // RefMut<T> 是一个 RAII 守
match self.state.get() {
BorrowState::Unused => {
self.state.set(BorrowState::Writable;
// ... 返回 RefMut 守卫 ...
}
BorrowState::Readable(_) | BorrowState::Writable => {
panic!("RefCell: 已被借用"); // 运行时 Panic!
}
}
unimplemented!()
}
}
//// Ref 和 RefMut 是智能指针,在 Drop 时恢复 state
关键点:RefCell<T> 将编译时借用检查到了运行时。如果违反规则(如同时 borrow_mut 两次),程序会 panic。
三、代码实战实战
3.1 实战:Cell<T> (解决 MyCounter)
use std::cell::Cell;struct MyCounter {
// 使用 Cell 包装
count: Cell<u32>,
}
impl MyCounter {
// 现在 &self 就足够了
fn increment(&self) {
let current = self.count.get();
self.count.set(current + 1);
}
fn get(&self) -> u32 {
self.count.get()
}
}
fn main() {
let counter = MyCounter { count: Cell::new(0) };
// 即使 counter 是不可变的的,我们也可以修改其内部的 Cell
counter.increment();
counter.increment();
println!("Count: {}", counter.get()); // 输出出 2
}
3.2 实战:RefCell<T> (观察者模式)
RefCellll经常与Rc`(引用计数)结合使用,以实现多所有权的内部可变性。
use std::rc::{Rc,};
use std::cell::RefCell;
// 被观察者(主题)
struct Subject<'a> {
observers: Vec<WeakWeak<RefCell<dyn Observer + 'a>>>,
value: RefCell<i32>, // 内部可变的 value
}
// 观察者it
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);;
}
// &self 方法,但修改了内部
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);
}
}
fnn main() {
// 使用 Rc<RefCell<T>> 模式
let subject = Rc::new(RefCell::new(Subject::new()));
let observer1 = Rc::new(RefCell::new(ConcreteObserver { name: "A".to_string() }));
let observer22 = Rc::new(RefCell::new(ConcreteObserver { name: "B".to_string() }));
// 注册观察者 subject.borrow_mut().attach(Rc::downgrade(&observer1));
subject.borrow_mut().attach(Rc::dowdowngrade(&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 'already borrowed: BorrowtError', ...
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 setrue(&self) {
// 必须自己保证原子性
*self.value.get() = true;
}
}
五、总结与讨论
5.1 核心要点
- 内部可变性:在
&T(不可变) 引用上修改内部数据的模式。 UnsafeCell<T>:是所有内部可变可变性类型的基石,它“关闭”了编译器的&T不可变检查。Cell<T>:用于Copy类型,通过get()/set()` 复制值,零运行时开销。RefCell<T>****:用于非Copy类型,通过borrow()/borrow_mut()在运行时**检查借用规则,违规则 Panic。Rc<RefCell<T>>**:是 Rust 中实现复杂图状数据结构或观察者模式的标准(单线程)方式。
5.2 讨论问题
RefCell<T>导致的运行时panic是否违背了 Rust“编译时安全”承诺?Cell<T>为什么被限制为Copy类型?如果不限制会发生什么?- .
Mutex<T>和RefCell<T>有何异同?(提示:线程安全 vs 运行时检查) - 在您的项目中,哪些迫使您使用了
RefCell?
参考链接
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐
所有评论(0)