首页 / Rust 入门教程 / Rc<T> 引用计数

Rust 入门教程

Rc<T> 引用计数

本教程共 78 篇 · 第 62 篇 · 更新于 2026-08-08 · 约 10 分钟阅读

RustRust 入门教程RcArc引用计数智能指针多所有权克隆

本节目标:理解 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,底层字符串还是同一份。ab 因此共享同一个字符串,两个计数都显示 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。当 ab 最终也离开作用域、计数归零,底层字符串才真正被清理。

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 数据没消失——因为 gadget1gadget2 还各握着一个 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_countRc 还有 weak_count 反映当前有多少个 Weak 指向它。Rc::downgrade 产生 Weak,它不阻止数据释放;想用时 upgradeOption<Rc<T>>,拿不到说明数据已没了。后面讲到引用循环(第 64 章)时,Weak 是打破循环的关键,这里先混个脸熟。

Note

观察计数不必写在正式代码里,调试时临时打印 Rc::strong_count / Rc::weak_count 非常管用——它能告诉你”这份数据现在到底被几个人指着”,是排查泄漏和生命周期问题的第一手线索。

把 Rc 想成”带计数的共享所有权管家”就对了:它记下有多少人需要这份数据,最后一个人离开时才关门。理解了计数这条线,下一章的 RefCell 和再后面的 Weak 都好懂。

下一章我们看 RefCell<T> 与”内部可变性”,解决 Rc 不能改的痛点。