背景
这是rust九九八十一难的第三篇,说下Rc
和RefCell
。最初想分开整理,但这俩经常一起用,所以集中说。这俩都是智能指针,侧重点不同,Rc
偏向管理所有权,RefCell
管理内部可变性。(什么是智能指针?智能指针 = 内存管理 + 额外功能,可以参考上一篇介绍Box的文章)。最初用这俩晕的不行,为了念头通达,研究了下用法和原理。

一、简单入门
跟以前一样,先举三个例子,看下怎么用:
1. 只用 Rc
(多所有权共享)
rust
use std::rc::Rc;
#[derive(Debug)]
struct Person {
name: String,
age: u32,
}
fn main() {
let alice = Rc::new(Person { name: "Alice".to_string(), age: 30 });
let bob = Rc::clone(&alice);
let charlie = Rc::clone(&alice);
println!("alice = {:?}, bob = {:?}, charlie = {:?}", alice, bob, charlie);
println!("Rc strong count = {}", Rc::strong_count(&alice)); // 3
}
Rc
允许多个变量共享Person。alice不可变,如果需要可变,就需要下面的 RefCell
。
2. 只用 RefCell
(内部可变性)
rust
use std::cell::RefCell;
#[derive(Debug)]
struct Counter {
value: i32,
}
fn main() {
let counter = RefCell::new(Counter { value: 0 });
counter.borrow_mut().value += 1;
counter.borrow_mut().value += 2;
println!("counter = {:?}", counter.borrow()); // value = 3
}
RefCell
允许在运行时对不可变变量进行可变借用
3. Rc
+ RefCell
(多所有权 + 可变性)
rust
use std::rc::Rc;
use std::cell::RefCell;
#[derive(Debug)]
struct Node {
name: String,
value: i32,
}
fn main() {
let shared_node = Rc::new(RefCell::new(Node { name: "Node2".to_string(), value: 10 }));
let a = Rc::clone(&shared_node);
let b = Rc::clone(&shared_node);
a.borrow_mut().value += 5;
println!("b sees: {:?}", b.borrow()); // 输出 value = 15
println!("Rc strong count = {}", Rc::strong_count(&shared_node)); // 3
}
这种组合常用在树或图结构中,既能多所有权,又可变。
二、 Rc:多所有权的超级共享指针
在 Rust 的世界里,内存管理被严肃地控制着。想象一下,如果你有一个宝贵的蛋糕(数据),但是你想让好几个朋友同时享用该怎么办?这时候就轮到 Rc(Reference Counted) 上场了。
Rc 的特点很简单:
- 多所有权:单线程环境,多个 Rc 可以共享同一个数据
- 自动释放:当最后一个 Rc 离开作用域,内存自动回收
- 不可变性:共享的数据默认不可修改
基本用法:
rust
use std::rc::Rc;
#[derive(Debug)]
struct Node {
value: i32,
children: Vec<Rc<Node>>,
}
fn main() {
let leaf1 = Rc::new(Node { value: 1, children: vec![] });
let leaf2 = Rc::new(Node { value: 2, children: vec![] });
let root = Rc::new(Node { value: 0, children: vec![Rc::clone(&leaf1), Rc::clone(&leaf2)] });
println!("{:?}", root);
}
✅ 说明:
-
Rc::clone(&leaf1)并不拷贝底层数据,只增加引用计数
-
当最后一个 Rc 离开作用域时,内存自动释放
但是,Rc 也有一些"脾气":
- 你不能直接修改它的内容
- 循环引用会导致内存泄漏
如果想修改数据,那么就需要 RefCell
上场了,内存泄漏后面也会说。
三、RefCell不可变 vs 可变借用规则:表面规矩,背地放纵
这里总结说下RefCell
的规则:
-
多个不可变借用可以共存
rustuse std::cell::RefCell; let v = RefCell::new(10); let r1 = v.borrow(); let r2 = v.borrow(); // ✅ 可以多个不可变借用 println!("{} {}", *r1, *r2);
-
只能有一个可变借用
rustlet v = RefCell::new(10); let mut m = v.borrow_mut(); // ✅ 独占可变借用 *m += 1;
-
不可变借用和可变借用不能同时存在
inilet v = RefCell::new(10); let r1 = v.borrow(); let mut m = v.borrow_mut(); // 💥 panic: already borrowed
编译器不会报错 ✅
运行时检查到「一边一堆人看,一边又有人乱改」 ❌ → 直接 panic
-
借用计数在运行时维护
borrow()
会把「不可变借用计数」+1,类比上面观众borrow_mut()
要求当前「不可变借用计数=0 且 没有可变借用」,否则 panic。
所以并不是「RefCell 只能借用一次」,而是:
- 不可变借用:可以多次,只要没有可变借用。
- 可变借用:一次只能有一个,且不能和不可变借用共存。
四、Rc配合RefCell使用:两全其美
如果只用RefCell
:
ini
use std::cell::RefCell;
let cat = RefCell::new("喵".to_string());
let r1 = cat.borrow(); // ✅ 不可变借用
let r2 = cat.borrow(); // ✅ 多个不可变借用
// let m = cat.borrow_mut(); // ❌ panic!已有不可变借用存在
要同时满足 "多方拥有" 和 "可以修改" 两个需求,但 Rust 默认规则不允许这么做,所以就需要 Rc + RefCell
配合。
ini
use std::cell::RefCell;
use std::rc::Rc;
let cat = Rc::new(RefCell::new("喵".to_string()));
let owner1 = cat.clone();
let owner2 = cat.clone();
*owner1.borrow_mut() = "喵(霸气御姐)".to_string();
*owner2.borrow_mut() = "喵(甜美萝莉)".to_string();
Rc
:允许多个 owner 拥有同一份数据。
RefCell
:允许在运行时可变访问数据。
组合起来,你就可以:
- 多人持有同一个值。
- 每个人都能修改它(独占修改时会检查借用规则)。
当然,这里也有个问题,像猫突然有了「多份培养计划书」,每个人都有决策权。
- 主人1 把猫训练成「霸气系」;
- 主人2 把猫训练成「甜美型」;
- 猫自己心里想:我是谁?我在哪?我到底是几号人格?
从这里可以看出 RefCell
的意义:
「Rust 像深宅大院的高高围墙,但 RefCell
告诉你:其实有狗洞可以钻。
它有灵活性,但需要开发者自觉
五、循环引用问题:互不放手
1. 悲剧的来源
在rust代码里,A引用了B,B引用了A,结果互相等待,无法释放。举例如下:
rust
use std::rc::Rc;
use std::cell::RefCell;
struct Node {
value: i32,
next: RefCell<Option<Rc<Node>>>,
}
fn main() {
let a = Rc::new(Node { value: 1, next: RefCell::new(None) });
let b = Rc::new(Node { value: 2, next: RefCell::new(None) });
// a -> b
*a.next.borrow_mut() = Some(Rc::clone(&b));
// b -> a
*b.next.borrow_mut() = Some(Rc::clone(&a));
println!("a strong = {}", Rc::strong_count(&a)); // 2
println!("b strong = {}", Rc::strong_count(&b)); // 2
} // ❌ a 和 b 都不会被释放
- 结果:
- a 和 b 都互相强引用对方。
- 当作用域结束时,
Rc
的引用计数永远不归零。 - 析构函数 (
Drop
) 不会执行 → 内存泄漏。
2. 怎么解决:弱引用(Weak)
Rust 给的解药是 Weak
:
rust
use std::rc::{Rc, Weak};
use std::cell::RefCell;
struct Node {
value: i32,
parent: RefCell<Weak<Node>>, // 弱引用,避免循环
children: RefCell<Vec<Rc<Node>>>,
}
fn main() {
let parent = Rc::new(Node { value: 1, parent: RefCell::new(Weak::new()), children: RefCell::new(vec![]) });
let child = Rc::new(Node { value: 2, parent: RefCell::new(Weak::new()), children: RefCell::new(vec![]) });
*child.parent.borrow_mut() = Rc::downgrade(&parent); // 弱引用
parent.children.borrow_mut().push(Rc::clone(&child));
println!("parent strong = {}, weak = {}", Rc::strong_count(&parent), Rc::weak_count(&parent));
}
Weak
不会增加强引用计数。
当 Rc
全部释放后,Weak
会自动失效,不会导致内存泄漏。
3. 如何检测循环引用
- Valgrind (Linux / macOS):rust.biofan.org/memory/valg...
Rust 程序跑完以后,用 Valgrind
检查内存:
bash
cargo install cargo-valgrind
cargo valgrind run
如果有循环引用导致的内存泄漏,Valgrind 会在报告里指出「仍然可达的内存块」。
在官方的链接里可以看到使用的示例,除了循环引用,还可以检查其他内存问题
AddressSanitizer
(ASan):clang.llvm.org/docs/Addres...
ASan 是 clang/LLVM 提供的内存错误检测工具。 Rust 可以通过 cargo-leak
来使用它:
arduino
cargo install cargo-leak
cargo leak run
它能检测:
-
内存泄漏
-
use-after-free
-
double free
-
Miri (官方解释器工具):github.com/rust-lang/m...
Miri 可以检查 未定义行为,包括借用冲突、悬垂引用,但不直接报循环引用。 不过它能帮助你发现某些「RefCell 借用未释放」的问题。
arduino
cargo miri setup
cargo miri run
总结下:
Valgrind / heaptrack → 看堆里有没有垃圾没清掉。
ASan / cargo-leak → 动态监控,发现泄漏就报警。
Miri → 借用规则检查,侧面发现 RefCell 使用不当。
六、总结
除了循环引用,这俩有额外性能开销,每次 borrow()
/ borrow_mut()
都要更新内部的计数器(运行时检查),这比普通 &T
/ &mut T
慢。滥用会导致「Rust 退化成脚本语言」,如果你到处用 Rc<RefCell<T>>
,等于放弃了编译期安全保障,最后写出来的 Rust 代码,跟 Python 没啥区别(还不如 Python 方便)。尽量当个高手,在编译期就搞定一切,实在不行再用 Rc<RefCell<T>>
打个补丁。
本人公众号大鱼七成饱,历史文章会在上面同步
