从C到Rust:struct Cell / RefCell 线程内局部可变性

线程内局部可变性 ------ Cell / RefCell

一、为什么需要内部可变性

1.1 外部可变性的约束

外部可变性------&T(共享只读)和 &mut T(独占读写)不能共存------是 Rust 安全的基石。但有些合法模式无法通过这个规则表达。

1.2 三个典型场景

场景 1:引用计数

rust 复制代码
use std::rc::Rc;

  


let r = Rc::new(42); // 通过 &Rc<i32> 访问

let r2 = Rc::clone(&r); // Rc::clone 需要递增引用计数

// ↑ 但 r 是 &Rc<i32>------不可变引用

// Rc 内部必须通过 &self 修改引用计数

场景 2:缓存统计

rust 复制代码
struct Cache {

data: HashMap<u32, String>,

hits: u32,

}

  


impl Cache {

fn get(&self, key: u32) -> Option<&String> {

// 希望在这里更新 hits:self.hits += 1

// ❌ self 是 &self,hits 不可变

self.data.get(&key)

}

}

场景 3:测试替身

rust 复制代码
struct MockService {

call_count: u32,

}

  


impl MockService {

fn handle(&self, request: Request) -> Response {

// 希望在这里记录调用次数

// ❌ self 是 &self

Response::ok()

}

}

共同点:需要在 &self(共享引用、不可变)下修改内部状态。

1.3 解决方案:内部可变性

  • 外部可变性&mut T,编译期检查,零运行时开销

  • 内部可变性 :通过 Cell<T>RefCell<T>&self 下修改,把检查推迟到运行时

外部可变性 内部可变性
访问方式 &mut T &T + Cell/RefCell
检查时机 编译期 运行时
违反规则的后果 编译错误 panic
是否需要 &mut self

1.4 为什么 C 不需要内部可变性

c 复制代码
// C 中不存在"内部可变性"的概念

struct Cache {

struct hashmap* data;

int hits;

};

  


int cache_get(struct Cache* cache, int key) {

cache->hits++; // ✅ 任何时候都能改

// C 不存在"只能读"的引用------所有指针都可写

}

C 不需要内部可变性,因为 C 中根本没有"不能修改"的保证 。Rust 需要内部可变性,正是因为 Rust 有"不能修改"的保证(&T),而有些合法场景需要在共享引用下修改。


二、Cell<T> ------ Copy 类型的内部可变性

2.1 基本用法

rust 复制代码
use std::cell::Cell;

  


let c = Cell::new(42);

  


println!("{}", c.get()); // 42

c.set(100); // 通过 &Cell<i32> 修改

println!("{}", c.get()); // 100

关键:setget 都只需要 &self

rust 复制代码
impl<T> Cell<T> {

pub fn set(&self, val: T); // ← 只需 &self

pub fn get(&self) -> T where T: Copy; // ← 只需 &self

}

2.2 为什么 Cell 是安全的

rust 复制代码
let c = Cell::new(42);

let r1 = &c;

let r2 = &c;

  


r1.set(100);

r2.set(200);

println!("{}", c.get()); // 200

安全的原因:

sql 复制代码
1. T 必须是 Copy ------ set 和 get 都是直接取值/存值

2. get() 返回副本,不是内部数据的引用

3. set() 替换整个值,不产生内部引用

4. 不存在"持有内部引用时修改值"的可能性

(因为 Cell 从不返回 &T)

Cell 通过限制为 Copy 类型来保证安全。 如果 T 不是 Copy(如 String),get() 需要返回引用------那就需要 RefCell

2.3 零成本抽象

rust 复制代码
use std::mem::size_of;

  


println!("{}", size_of::<i32>()); // 4

println!("{}", size_of::<Cell<i32>>()); // 4 ------ 无额外开销

Cell<i32> 在内存中就是一个 i32get/set 编译为普通的读/写指令。

2.4 常见用法

rust 复制代码
// 计数器

struct Counter {

count: Cell<u32>,

}

  


impl Counter {

fn increment(&self) { // &self,不是 &mut self

self.count.set(self.count.get() + 1);

}

fn value(&self) -> u32 {

self.count.get()

}

}

  


let c = Counter::new();

let r1 = &c;

let r2 = &c;

r1.increment();

r2.increment();

assert_eq!(c.value(), 2);
rust 复制代码
// 与 Rc 配合共享状态

use std::rc::Rc;

use std::cell::Cell;

  


let shared: Rc<Cell<i32>> = Rc::new(Cell::new(0));

let r1 = Rc::clone(&shared);

let r2 = Rc::clone(&shared);

  


r1.set(10);

r2.set(20);

println!("{}", shared.get()); // 20

三、RefCell<T> ------ 运行时借用检查

3.1 基本用法

rust 复制代码
use std::cell::RefCell;

  


let rc = RefCell::new(String::from("hello"));

  


// 通过 borrow() 获取不可变引用

let r1 = rc.borrow(); // 返回 Ref<String>

println!("{}", r1); // "hello"

  


// 通过 borrow_mut() 获取可变引用

let mut r2 = rc.borrow_mut(); // 返回 RefMut<String>

r2.push_str(", world");

println!("{}", r2); // "hello, world"

3.2 签名

rust 复制代码
impl<T: ?Sized> RefCell<T> {

pub fn borrow(&self) -> Ref<'_, T>;

pub fn borrow_mut(&self) -> RefMut<'_, T>;

}

两者都只需要 &self

3.3 运行时借用规则

rust 复制代码
let rc = RefCell::new(String::from("hello"));

  


let r1 = rc.borrow(); // ✅ 不可变引用(计数=1)

let r2 = rc.borrow(); // ✅ 另一个不可变引用(计数=2)

  


// let mut r3 = rc.borrow_mut(); // ❌ 运行时 panic!

// 已有不可变引用,不能借可变引用

  


drop(r1);

drop(r2);

  


let mut r3 = rc.borrow_mut(); // ✅ 现在可以了

规则与编译期相同:

  • 多个 borrow() 可以共存

  • borrow()borrow_mut() 不能共存

  • 多个 borrow_mut() 不能共存

违反规则时------panic,不是 UB。

3.4 运行时开销

rust 复制代码
// RefCell 的内部结构

struct RefCell<T> {

value: T,

borrow: Cell<BorrowFlag>, // 借用计数:0 / n / -1

}

每次 borrow() / borrow_mut()

  1. 检查当前借用标志

  2. 如果合法,更新标志

  3. 返回 Ref / RefMut(drop 时恢复标志)

开销:一次整数检查 + 一次赋值。

3.5 Rc + RefCell:经典组合

rust 复制代码
use std::rc::Rc;

use std::cell::RefCell;

  


let shared: Rc<RefCell<Vec<i32>>> = Rc::new(RefCell::new(vec![]));

  


let r1 = Rc::clone(&shared);

let r2 = Rc::clone(&shared);

  


r1.borrow_mut().push(1);

r2.borrow_mut().push(2);

  


println!("{:?}", shared.borrow()); // [1, 2]

Rc 提供共享所有权,RefCell 提供共享可变性。

3.6 panic 与 try_borrow

borrow_mut() 在违反规则时直接 panic:

rust 复制代码
let rc = RefCell::new(String::from("hello"));

let r1 = rc.borrow_mut();

let r2 = rc.borrow_mut(); // ⚠️ panic: already borrowed

try_borrow_mut() 是 panic-free 版本:

rust 复制代码
match rc.try_borrow_mut() {

Ok(mut r) => r.push_str("ok"),

Err(_) => println!("暂时借不到"),

}

四、&mut T vs Cell vs RefCell 对比

维度 &mut T Cell<T> RefCell<T>
检查时机 编译期 无需检查 运行时
违反规则 编译错误 不存在 panic
T 的要求 任意 Copy 任意
获取值 直接 *r get() 返回副本 borrow() 返回引用
修改方式 *r = val set(val) borrow_mut()
运行时开销 每次 borrow 一次检查
能否在 &self 下用
线程安全 Send !Sync !Sync

选型流程

swift 复制代码
需要内部可变性(在 &self 下修改)?

│

├── T 是 Copy 吗?

│ ├── ✅ → Cell<T>(零运行时开销)

│ └── ❌ → RefCell<T>(运行时检查)

│

├── 还需要共享所有权?

│ └── ✅ → Rc<RefCell<T>>

│

└── 还需要跨线程?

└── → 见「跨线程可变性」章节

五、与 C 程序员的对话

"C 不需要这些,因为所有指针都能写"

C 程序员:"在 C 中你任何时候都能写任何数据。Cell 和 RefCell 在 C 中没意义。"
Rust :"这正是关键区别。C 中所有数据都是可写的,所以 C 中不存在'共享不可变'的保证。Rust 有 &T 保证------'这个引用指向的数据不会被任何人修改'。但有些场景(缓存、计数)确实需要在共享引用下修改,所以 Rust 提供了 Cell 和 RefCell 作为受控的例外。"

c 复制代码
// C ------ 没有"共享不可变"

const struct Cache* cache = get_cache();

// 谁能保证没人修改它?另一个线程可能有非 const 指针指向同一内存
rust 复制代码
// Rust ------ &T 是真正的只读

let cache: &Cache = get_cache();

// 编译器保证不会有人修改 cache 的数据

// 但如果字段用了 Cell/RefCell,那些字段可以变

"运行时检查不就是开销吗?"

Rust :"是开销。但大部分代码用 &mut T(编译期检查,零开销)。只有在确实需要在 &self 下修改时才使用 RefCell。C 中没有 borrow 检查的开销,但有调试内存 bug 的开销------那通常比 borrow 检查贵得多。"

erlang 复制代码
95% 的代码:&mut T → 零运行时开销

5% 的代码:Cell → 零运行时开销(仅限 Copy)

<1% 的代码:RefCell → 有运行时开销

六、小结

核心概念

r 复制代码
外部可变性:&mut T --- 编译期检查,零开销,独占访问

↓ 不够用时:

线程内内部可变性:

├── Cell<T> --- 仅 Copy 类型,零开销,取值/存值

└── RefCell<T> --- 任意类型,运行时借用检查

C vs Rust

C Rust
共享引用时能改吗 可以(所有指针都可写) 不能(&T 保证),除非用 Cell/RefCell
内部可变性 不需要(一切皆可改) 需要(因为 &T 保证不可改)
违反规则的后果 无(也没有规则) 编译错误(&mut T)或 panic(RefCell)

一句话总结 :Rust 通过 &T 提供了真正的"共享不可变"保证,但有些场景需要在共享引用下修改数据。CellRefCell 在单线程中提供这种能力:Cell 用于 Copy 类型(零开销),RefCell 用于任意类型(运行时检查)。它们把 C 中"一切皆可写"的混乱变成"默认不可写,显式声明即可写"的秩序。

相关推荐
Kapaseker2 小时前
Rust 是如何处理异常的
rust·kotlin
铅笔侠_小龙虾4 小时前
Rust 学习(8)-切片类型
学习·rust·c#
程序员爱钓鱼7 小时前
Rust 函数与返回值详解:参数、表达式与返回类型
后端·rust
花褪残红青杏小13 小时前
Rust图像处理第17节-Julia 朱利亚分形:拖动常数 c 看图实时变化
rust·webassembly·图形学
An_s17 小时前
rust开发wasm与浏览器(js)交互
javascript·rust·wasm
Mr -老鬼19 小时前
Zed WASM扩展开发全套踩坑记录(WASI适配、LSP插件开发避坑指南)
rust·wasm·lsp·zed·ai ide·语言服务器
糯米导航21 小时前
Rust + ONNX Runtime 构建生产级 AI 推理服务:从零到压测
开发语言·人工智能·rust
Arman37681 天前
rusty-cat 分片并发上传完全指南
rust·vibecoding
独孤留白1 天前
从C到Rust:mut 可变性,与恶魔战斗的利剑
rust