Rc<T> 引用计数
本教程共 78 篇 · 第 62 篇 · 更新于 2026-08-08 · 约 10 分钟阅读
本节目标:理解 Rc
如何打破”一个值只有一个所有者”的铁律,让一份数据同时拥有多个主人;学会用 Rc::clone 只增计数不拷底层数据,用 Rc::strong_count 观察计数变化,并分清单线程的 Rc 和多线程安全的 Arc。
所有权规则说:一个值只能有一个主人。这在大多数情况没错,但有些场景它成了绊脚石。比如图结构里多个边指向同一个节点,节点要等到所有边都消失才该释放;再比如多个工具属于同一个主人。这种”共享所有权”的需求,靠 Rc<T> 来解决。
1-1 所有权唯一性带来的麻烦
看这段会报错的代码:
fn main() {
let s = String::from("hello");
let a = Box::new(s);
let b = Box::new(s); // 报错:s 的所有权已经给了 a
}
s 的所有权给了 a 后,b 再想拿就冲突了。所有权转移是”独占”的,不允许一份数据有两个主人。
Rc 是 Reference Counting(引用计数)的缩写。它内部记着一个计数器:数据被引用几次。计数归零,说明没人用了,数据才释放。这样一份数据就能被多处共享。
1-2 用 Rc 实现多所有权
use std::rc::Rc;
fn main() {
let a = Rc::new(String::from("hello"));
let b = Rc::clone(&a);
assert_eq!(2, Rc::strong_count(&a));
assert_eq!(Rc::strong_count(&a), Rc::strong_count(&b));
}
Rc::new 创建智能指针,计数从 1 起。Rc::clone(&a) 不是深拷贝——它只复制了智能指针、把计数加 1,底层字符串还是同一份。a 和 b 因此共享同一个字符串,两个计数都显示 2。
Tip共享 Rc 时请用
Rc::clone(&a)而不是a.clone()。两者效果一样,但Rc::clone读起来一眼就知道”这只是增加引用计数、几乎零成本”,不会让人误以为是昂贵的深拷贝。这是社区约定。
1-3 观察计数变化
用 Rc::strong_count 能实时看到计数。看它怎么随变量生灭变化:
use std::rc::Rc;
fn main() {
let a = Rc::new(String::from("test"));
println!("创建 a 后: {}", Rc::strong_count(&a)); // 1
let b = Rc::clone(&a);
println!("创建 b 后: {}", Rc::strong_count(&a)); // 2
{
let c = Rc::clone(&a);
println!("创建 c 后: {}", Rc::strong_count(&c)); // 3
} // c 离开这个块,计数减回 2
println!("c 离开后: {}", Rc::strong_count(&a)); // 2
}
c 在内部语句块里创建,离开块时因超出作用域被 drop,计数自动减 1。这得益于 Rc 实现了 Drop。当 a、b 最终也离开作用域、计数归零,底层字符串才真正被清理。
Note你直接看不到”归零那一下”,但原理是:最后一个所有者消失 → 计数变 0 → 智能指针和它指向的数据一并释放。生命周期在编译期就确定了,没有 GC 的运行时负担。
1-4 Rc 指向的数据不可变
Rc<T> 内部是对底层数据的不可变引用。你没法通过它修改数据,这完全符合借用规则:要么多个不可变借用,要么一个可变借用。Rc 给你的是”多个只读主人”。
真要改共享数据怎么办?单独用 Rc 不行,得配合带内部可变性的类型,比如后面的 RefCell<T>(单线程)或 Mutex<T>(多线程)。常见组合是 Rc<RefCell<T>>:前者管多所有权,后者管可修改。
1-5 综合例子:多个工具共享一个主人
use std::rc::Rc;
struct Owner {
name: String,
}
struct Gadget {
id: i32,
owner: Rc<Owner>,
}
fn main() {
let gadget_owner: Rc<Owner> = Rc::new(Owner { name: "工匠".to_string() });
let gadget1 = Gadget { id: 1, owner: Rc::clone(&gadget_owner) };
let gadget2 = Gadget { id: 2, owner: Rc::clone(&gadget_owner) };
drop(gadget_owner); // 释放其中一个 Rc<Owner>
// 仍可访问,因为 gadget1、gadget2 还各持有一个引用,计数尚未归零
println!("工具 {} 归 {} 所有", gadget1.id, gadget1.owner.name);
println!("工具 {} 归 {} 所有", gadget2.id, gadget2.owner.name);
}
这里 gadget_owner 被 drop 后,Owner 数据没消失——因为 gadget1 和 gadget2 还各握着一个 Rc<Owner>,计数仍是 2。直到它们也离开作用域,计数归零,数据才释放。如果改用普通借用,生命周期会非常难管理;Rc 让共享自然又安全。
1-6 单线程的 Rc 无力跨线程
试着把 Rc 塞进多线程会报错:Rc<String> cannot be sent between threads safely。根因是 Rc 没实现 Send——它的计数器不是原子操作,多线程下并发增减会数错。
use std::rc::Rc;
use std::thread;
fn main() {
let s = Rc::new(String::from("多线程"));
for _ in 0..3 {
let s = Rc::clone(&s);
thread::spawn(move || {
println!("{}", s);
});
}
} // 编译错误
1-7 Arc:线程安全的引用计数
把 Rc 换成 Arc(Atomic Rc,原子化引用计数)就安全了:
use std::sync::Arc;
use std::thread;
fn main() {
let s = Arc::new(String::from("多线程"));
for _ in 0..3 {
let s = Arc::clone(&s);
thread::spawn(move || {
println!("{}", s);
});
}
}
Arc 的计数用原子操作保证线程安全,API 和 Rc 几乎一模一样,只是引入路径不同:use std::sync::Arc。代价是原子操作有性能损耗,所以 Rust 让你自己选——单线程用 Rc 省开销,跨线程才用 Arc。
Warning别图省事全程用
Arc。原子计数的开销虽小,但在高频 clone 的热点路径上会累积。绝大多数程序跑在单线程里,用Rc就够了;只有真要跨线程共享才上Arc。
1-8 小结
Rc/Arc让一份数据拥有多个所有者,计数归零才释放,生命周期编译期确定。Rc::clone只增计数不拷数据,高效;只读,要改得配RefCell/Mutex。Rc单线程,Arc多线程安全;后者有原子开销。
Note
Rc实现了Deref,所以你能直接写gadget.owner.name而不必先解引用Rc——这正是上一章讲的 Deref 隐式转换在起作用。
1-9 引用计数的代价与误用
Rc 好用,但别把它当万能胶。它有几个真实代价:
第一,clone 虽便宜,也不是免费的。每次 Rc::clone 都要原子或普通地增减计数(单线程 Rc 是非原子的,略快但仍有一次内存写)。在每帧、每次循环都疯狂 clone 的热点路径上,这点开销会累积。能用引用 & 借就用借,别动不动就 clone 出新的所有者。
第二,Rc 绑死了堆分配。它背后一定是堆上的数据,没法像普通栈值那样灵活。如果只是临时共享一个小值,考虑直接传引用而不是包成 Rc。
Warning最危险的误用就是制造引用循环(下一章专讲)。两个 Rc 互相指着对方,计数永远归不了零,内存悄悄泄漏。只要你的数据结构里出现”双向持有 Rc”,立刻警觉:是不是该有一边降级成
Weak?
第三,别用 Rc 来”规避借用检查器的报错”。有时编译器报借用错误,新手图省事把值塞进 Rc 绕过去——这往往说明你的所有权设计本身该重新想。Rc 解决的是”真的需要共享所有权”的问题,不是”我不想理清楚借用关系”的借口。
Tip一个判断口诀:这份数据”被多人同时需要、且谁先结束不确定”,才上 Rc;如果只是函数间临时传递,引用或返回值更合适。想清楚了再用,Rc 才是利器而非负担。
1-10 弱引用计数也值得留意
除了 strong_count,Rc 还有 weak_count 反映当前有多少个 Weak 指向它。Rc::downgrade 产生 Weak,它不阻止数据释放;想用时 upgrade 成 Option<Rc<T>>,拿不到说明数据已没了。后面讲到引用循环(第 64 章)时,Weak 是打破循环的关键,这里先混个脸熟。
Note观察计数不必写在正式代码里,调试时临时打印
Rc::strong_count/Rc::weak_count非常管用——它能告诉你”这份数据现在到底被几个人指着”,是排查泄漏和生命周期问题的第一手线索。
把 Rc 想成”带计数的共享所有权管家”就对了:它记下有多少人需要这份数据,最后一个人离开时才关门。理解了计数这条线,下一章的 RefCell 和再后面的 Weak 都好懂。
下一章我们看 RefCell<T> 与”内部可变性”,解决 Rc 不能改的痛点。