Skip to content

智能指针

本章讲 Rust 里「指针」的完整工具箱:谁拥有数据、谁能改数据(内部可变性)、 谁能在多线程里共享数据。前置知识:所有权与移动trait泛型与生命周期

本章目标

  • 能看着一张需求,从 Box / Rc / Arc / RefCell / Cell / Mutex 里挑出正确的组合。
  • 能自己实现一个带 DerefDrop 的智能指针(smart pointer),并解释解引用强制转换 (deref coercion)是编译器怎么插进去的。
  • 能解释 Rc 为什么不能改数据、RefCell 为什么能在 &self 方法里改数据,以及后者的代价。
  • 能用 Weak 打破 Rc 引用循环(reference cycle),并用 strong_count / weak_count 证明没泄漏。
  • 能说清闭包(closure)的三种捕获方式,知道什么时候必须写 movemoveCopy 类型意味着什么。
  • 能独立定位 E0382 / E0507 / E0373RefCell 运行时 panic 的成因。

💡 对照:Rust 的智能指针不是「给 GC 语言加语法糖」,而是把「所有权怎么分、 数据能不能改、跨不跨线程」这三件事变成类型系统里的三个正交维度。C++ 里这三件事 靠约定和注释,Rust 里靠类型——选错类型就编译不过或运行期 panic。


指针总览:一张表决定选型

标准库里这些类型都实现了 Deref,所以都能用 *p 取值、都能自动解引用调用方法, 它们都算「智能指针」。区别只在所有权语义可变性线程安全运行时开销

类型所有权数可变性线程安全运行时开销典型用途
&T多个(借用,不拥有)只读Send/Sync 取决于 T无(就是一个机器字)只读传参、切片视图
&mut T唯一(借用,不拥有)独占可变Send,不同步 Sync可变传参,编译期保证独占
Box<T>唯一可变(DerefMutT一次堆分配递归类型、大对象转移、trait 对象
Rc<T>多个(引用计数)只能只读!Send !Sync非原子计数增减单线程共享只读数据、图/树
Arc<T>多个(原子计数)只能只读Send+Sync(若 T: Send+Sync原子计数(比 Rc 慢)跨线程共享只读数据
Cell<T>唯一(但可共享 &可整体替换!Sync近乎为零Copy 小字段的计数器、状态位
RefCell<T>唯一(但可共享 &运行时可读写!Sync借用计数(非原子)单线程共享可变数据
Mutex<T>唯一(但可共享 &运行时可读写Sync(若 T: Send加锁/解锁、可能阻塞跨线程共享可变数据
RwLock<T>唯一(但可共享 &运行时可读写Sync读写锁,读者并行读多写少的跨线程共享
Cow<'a, T>借用或拥有二选一写时克隆T几乎为零,写时一次分配可能不改就返回借用的 API
Weak<T>零个(弱引用)只读升强Rc/ArcRc 同一控制块打破循环、缓存、观察者
OnceLock<T>全局唯一初始化一次后只读Send+Sync一次原子判断进程级单例、延迟初始化
LazyLock<T>全局唯一初始化一次后只读Send+Sync一次原子判断用表达式初始化的静态表

「运行时开销」一列的读法:Box / &T / &mut T零或一次分配Rc / Arc 每次 clone 要改计数;RefCell 每次借用要改计数; Mutex / RwLock 每次访问都要过操作系统同步原语。

栈上指针 + 堆上数据长什么样

栈(函数栈帧,编译期已知大小)            堆(运行时分配,大小任意)

let b: Box<String> = Box::new(String::from("hi"));

  b                ┌──────────┐            ┌─────────────────────────┐
 ┌────────┐        │ ptr      │──────────▶ │ String 结构体(24 字节)│
 │ 0x7ff… │───┐    ├──────────┤            │  ptr ───────────────────┼──▶ ┌───┬───┐
 └────────┘   │    │ len = 2  │            │  len = 2                │    │ h │ i │
  8 字节指针  └───▶│ cap = 2  │            │  cap = 2                │    └───┴───┘
                   └──────────┘            └─────────────────────────┘
                   栈上的 String 本身        Box 指向的 String 在堆上

Box<T> 本身永远是 8 字节(一个指针),和 T 多大无关——这就是「装箱」的全部意义: 把编译期未知或无法确定的大小挪到运行期。

递归类型为什么必须 Box:

enum List { Cons(i32, List), Nil }        ← 编译器算不出大小,报 E0072

  List 的大小 = max(i32 + List, 0) = ∞     (自指,无解)

加一层指针就有解了:

enum List { Cons(i32, Box<List>), Nil }

  List 的大小 = max(4 + 8, 0) = 16 字节(含对齐)  ← 已知!
   ├─ i32 占 4 字节
   └─ Box<List> 占 8 字节,指向堆上的下一个 List

🧠 原理size_of::<T>() 必须是编译期常量,因为栈帧大小在生成代码时就定死了。 只有指针/引用(&BoxRcArc)的大小与所指数据无关,所以它们能「打断」 无限递归的尺寸计算。这也解释了 &[T](宽指针:指针 + 长度)为什么是 16 字节。

💡 对照:Java 里所有对象变量天然就是「指针」,栈上只有引用,所以没有递归类型的 尺寸问题;代价是每次访问都要多一次间接寻址,且装箱(boxing)的时机由 GC 决定。 Rust 让你显式选择:想要引用语义就写 Box,不写就在栈上直接展开。


Box<T>:唯一的堆所有权

Box<T> 是最朴素的智能指针:一个指向堆上 T 的指针,独占所有权,出了作用域释放堆内存。

rust
fn main() {
    let b = Box::new(5); // 5 在堆上,b 在栈上
    println!("b = {}", *b);
    println!("b + 1 = {}", *b + 1);
    // b 在这里离开作用域,堆上的 5 被释放
}

装箱的动机:递归类型与「转移大对象」

rust
#[derive(Debug)]
enum List {
    Cons(i32, Box<List>), // 必须是 Box:否则 List 尺寸无限大(E0072)
    Nil,
}

fn main() {
    // Cons(1, Cons(2, Cons(3, Nil)))
    let list = List::Cons(
        1,
        Box::new(List::Cons(2, Box::new(List::Cons(3, Box::new(List::Nil))))),
    );
    println!("{list:?}");
}

输出:Cons(1, Cons(2, Cons(3, Nil)))

如果写成 Cons(i32, List),编译器会直接拒绝:

error[E0072]: recursive type `List` has infinite size
 --> e0072.rs:2:1
  |
2 | enum List {
  | ^^^^^^^^^
3 |     Cons(i32, List),
  |               ---- recursive without indirection
  |
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
  |
3 |     Cons(i32, Box<List>),
  |               ++++    +

第二个动机是避免大对象搬家。把一个 4 KB 的结构体按值传来传去,每次移动都要 memcpyBox::new 只有一次分配,之后传的永远是一个 8 字节指针:

rust
struct Big {
    data: [u64; 512], // 4 KiB
}

fn consume(_big: Big) {}

fn main() {
    let boxed = Box::new(Big { data: [0; 512] });
    consume(*boxed); // 仍然会拷贝 4 KiB:解引用后是按值传
    // 想避免拷贝就传 Box 本身或 &Big
}

⚠️ 陷阱*boxed 传给按值参数会把值从堆上搬出来(栈拷贝),不是零成本。 要么让函数收 &Big,要么让函数收 Box<Big>。只有 Box<T> 本身移动才是廉价的。

Box<dyn Trait>:trait 对象的标准载体

dyn Trait不定长类型(unsized type),不能直接放在栈上、不能当返回值,必须装进 指针里。Box<dyn Trait> 是最常用的一种。

rust
trait Shape {
    fn area(&self) -> f64;
    fn name(&self) -> &str;
}

struct Circle {
    r: f64,
}
struct Square {
    side: f64,
}

impl Shape for Circle {
    fn area(&self) -> f64 {
        std::f64::consts::PI * self.r * self.r
    }
    fn name(&self) -> &str {
        "circle"
    }
}

impl Shape for Square {
    fn area(&self) -> f64 {
        self.side * self.side
    }
    fn name(&self) -> &str {
        "square"
    }
}

fn main() {
    // 不同具体类型放进同一个 Vec,靠 Box<dyn Shape> 抹掉类型
    let shapes: Vec<Box<dyn Shape>> = vec![
        Box::new(Circle { r: 1.0 }),
        Box::new(Square { side: 2.0 }),
    ];
    for s in &shapes {
        println!("{} -> {:.3}", s.name(), s.area());
    }
}

输出:

circle -> 3.142
square -> 4.000

Box<dyn Shape> 是个胖指针(fat pointer):16 字节 = 数据指针 + 虚表(vtable)指针。

Box<dyn Shape>(16 字节)
 ┌────────────┬────────────┐
 │ data ptr   │ vtable ptr │
 └─────┬──────┴─────┬──────┘
       │            └──▶ ┌──────────────────────┐
       │                 │ drop_in_place        │
       │                 │ size / align         │
       │                 │ area()  ──▶ Circle::area
       │                 │ name()  ──▶ Circle::name
       │                 └──────────────────────┘

   堆上的 Circle { r: 1.0 }

🧠 原理dyn Trait 的「动态分发」就是运行期查虚表跳转,和 C++ 虚函数一样; 而泛型 fn f<T: Shape>(x: T)静态分发,每个具体类型生成一份代码(单态化), 没有虚表、也没有指针间接。选 dyn 还是泛型,本质是「代码体积 vs 运行期灵活性」。

🚀 进阶dyn Trait 默认带 + 'static 约束,想放带生命周期的东西必须写 Box<dyn Shape + 'a>Box<dyn Fn(i32) -> i32> 是 「闭包(Closure)」一节闭包返回值的标准写法。

Box::leak:故意泄漏

Box::leak(b)Box<T> 的所有权「漏」掉,返回 &'a mut T。这个引用永远不释放, 生命周期可以长达 'static

rust
fn config_name() -> &'static str {
    // 启动时初始化一次、活到进程结束的配置字符串
    let boxed: Box<str> = Box::from("my-app");
    Box::leak(boxed) // 返回 &'static mut str,自动协变为 &'static str
}

fn main() {
    println!("{}", config_name());
}

输出:my-app

Box::leak有意的内存泄漏(leak),不是 bug:Rust 允许泄漏,泄漏只是「不释放」, 不会造成悬垂指针或 UB(详见「引用循环与内存泄漏:用 Weak 打破环」)。它常用于「初始化一次、活到进程退出」的配置/单例。

⚠️ 陷阱:循环里调用 Box::leak 就是真的漏内存(比如处理请求时泄漏每个请求的字符串)。 需要「初始化一次」的共享数据,用 「Rc<RefCell<T>>:单线程的共享可变」一节的 OnceLock / LazyLock,它们是线程安全且 不会无限增长的。

Box<[T]>Box<str>:定长化

Box<[T]> 是「拥有所有权的切片」,Box<str> 是「拥有所有权的字符串切片」。 它们把 Vec<T> / String容量字段丢掉,大小 = 16 字节(指针 + 长度),不能再增长。

rust
fn main() {
    let v = vec![1, 2, 3, 4];
    let boxed: Box<[i32]> = v.into_boxed_slice(); // 丢掉多余的 capacity
    println!("{boxed:?} len = {}", boxed.len());

    let s: Box<str> = "hello".into();
    println!("{} len = {}", s, s.len());
}

输出:

[1, 2, 3, 4] len = 4
hello len = 5

何时不必 Box

场景该不该 Box
结构体字段是另一个结构体,且大小固定不用,直接内联(缓存友好,少一次跳转)
递归类型(enum、链表节点)必须 Box(或 Rc/Arc),否则 E0072
需要在运行期决定具体类型(插件、异构集合)必须 Box<dyn Trait>
只是想把大对象传出去优先传 &T,而不是 Box<T>
返回值要逃出函数直接返回值即可,Rust 有返回值优化;Box 只用于打破尺寸/生命周期限制
想让数据「活到进程结束」Box::leak,或者更好的 OnceLock

💡 对照:C# / Java 里 class 的实例天生在堆上、struct 天生在栈上;Rust 只有 Box::new 显式装箱。Go 的「逃逸分析」帮你自动决定堆还是栈,Rust 则是由你写 Box 决定,因此没有「编译器偷偷把变量搬到堆上导致性能意外」这种情况。


DerefDrop 与自定义智能指针

*b 是语法糖

*b 会调用 Deref::deref 拿到 &T,再解引用。Deref 的定义极简:

rust
pub trait Deref {
    type Target: ?Sized;
    fn deref(&self) -> &Self::Target;
}

pub trait DerefMut: Deref {
    fn deref_mut(&mut self) -> &mut Self::Target;
}
rust
use std::ops::Deref;

fn main() {
    let b = Box::new(String::from("hi"));
    println!("{}", *b); // *(b.deref())
    println!("{}", *b); // String -> str 的是 str 的 Deref,与 Box 无关
}

输出:

hi
hi

注意 Deref 只给不可变访问。Box<T> 额外实现了 DerefMut,所以 *b = x 能编译; Rc<T> 没有 DerefMut,所以 *rc = x 编译不过——这是设计,不是遗漏(见「T 必须是 Sized(但可以是 [T] / str)」)。

解引用强制转换链

编译器在需要 &U 但拿到 &T 时,会尝试插入 deref(),并且可以连续插多次

MyBox<String>  ──deref()──▶  String  ──deref()──▶  str
     &MyBox<String>            &String              &str
rust
use std::ops::Deref;

struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

impl<T> Deref for MyBox<T> {
    type Target = T;
    fn deref(&self) -> &T {
        &self.0 // 返回内部值的引用,所有权仍在 MyBox 手里
    }
}

fn hello(name: &str) {
    println!("hello {name}");
}

fn str_len(s: &str) -> usize {
    s.len()
}

fn main() {
    let mb = MyBox::new(String::from("world"));
    hello(&mb); // &MyBox<String> -> &String -> &str,编译器插了两次 deref
    println!("len = {}", str_len(&mb));
    println!("upper = {}", mb.to_uppercase()); // 方法调用也自动解引用
}

输出:

hello world
len = 5
upper = WORLD

🧠 原理:强制转换只发生在已知目标类型的位置(函数实参、方法接收者、 let 带类型标注等)。hello(&mb) 中编译器知道 hello&str,于是插入 Deref::deref。如果只写 let x = &mb; 而不给出目标类型,就不会发生转换。

Drop 与释放顺序

Drop::drop(&mut self) 在值离开作用域时被编译器自动插入调用。关键规则:

  1. 同作用域内,变量按声明顺序的逆序 drop(后声明的先释放,像栈一样)。
  2. 结构体字段按声明顺序 drop(注意:和变量规则相反)。
  3. 实现了 Drop 的类型,字段仍然会被自动 dropdrop 里只该写「额外清理」。
  4. Drop::drop 不能手动调用;想提前释放用 std::mem::drop(value)(它按值拿走并立刻析构)。
rust
struct Noisy(&'static str);

impl Drop for Noisy {
    fn drop(&mut self) {
        println!("drop {}", self.0);
    }
}

struct Pair {
    first: Noisy,
    second: Noisy,
}

fn main() {
    let _a = Noisy("a(先声明)");
    let _b = Noisy("b(后声明)");

    println!("--- 进入内层作用域 ---");
    {
        let _inner = Noisy("inner");
    } // inner 在这里被 drop
    println!("--- 离开内层作用域 ---");

    let _p = Pair {
        first: Noisy("first 字段"),
        second: Noisy("second 字段"),
    };

    let c = Noisy("c");
    drop(c); // 手动提前释放,等价于 mem::drop
    println!("--- main 结束,剩余变量开始逆序释放 ---");
}

输出:

--- 进入内层作用域 ---
drop inner
--- 离开内层作用域 ---
drop c
--- main 结束,剩余变量开始逆序释放 ---
drop second 字段
drop first 字段
drop b(后声明)
drop a(先声明)

std::mem::dropDrop::drop 的区别是新手最容易混的一对:

std::mem::drop(x)Drop::drop(&mut self)
是什么标准库函数 pub fn drop<T>(_x: T) {}trait 方法
怎么调用drop(c);不能手动调用(c.drop() 报 E0040)
做什么按值接收 x,函数结束时 x 被析构你自己实现的清理逻辑
用途提前结束作用域、释放锁守卫自定义资源释放(文件、socket)

⚠️ 陷阱mem::drop 对实现了 Copy 的类型是空操作——Copy 类型没有所有权转移, drop(3i32) 只是复制了一个 3。想「提前释放」Copy 变量是不可能的,也不需要。

⚠️ 陷阱Drop 里 panic 而外层正在 panic,会直接 abort 进程(double panic)。 Drop 实现里不要写会 panic 的代码;锁守卫建议 .unwrap_or_else(|e| e.into_inner())

完整的自定义智能指针

下面这个 MyBox<T> 实现了 DerefDerefMutDropDisplay,覆盖了语法糖、DerefMutDrop 三个点 的全部机制:

rust
use std::fmt;
use std::ops::{Deref, DerefMut};

struct MyBox<T: fmt::Display>(T);

impl<T: fmt::Display> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

impl<T: fmt::Display> Deref for MyBox<T> {
    type Target = T;
    fn deref(&self) -> &T {
        &self.0
    }
}

impl<T: fmt::Display> DerefMut for MyBox<T> {
    fn deref_mut(&mut self) -> &mut T {
        &mut self.0
    }
}

impl<T: fmt::Display> Drop for MyBox<T> {
    fn drop(&mut self) {
        // 真实场景这里会释放文件句柄 / socket / 归还连接池
        println!("释放 MyBox: {}", self.0);
    }
}

impl<T: fmt::Display> fmt::Display for MyBox<T> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "MyBox({})", self.0)
    }
}

fn hello(name: &str) {
    println!("hello {name}");
}

fn main() {
    let mb = MyBox::new(42);
    println!("解引用 = {}", *mb);
    println!("显示 = {mb}");

    let mut text = MyBox::new(String::from("world"));
    hello(&text); // 解引用强制转换:&MyBox<String> -> &str
    text.push_str("!"); // DerefMut:走 String 的方法
    println!("改后 = {text}");

    *text = String::from("重新赋值"); // *text = ... 也走 DerefMut
    println!("重赋 = {text}");

    drop(mb);
    println!("mb 已提前释放");
}

输出:

解引用 = 42
显示 = MyBox(42)
hello world
改后 = MyBox(world!)
重赋 = MyBox(重新赋值)
释放 MyBox: 42
mb 已提前释放
释放 MyBox: 重新赋值

Rc<T>:单线程的共享所有权

Rc<T>(reference counting,引用计数)让多个所有者共享同一份只读数据。 数据在堆上,旁边有一个「控制块(control block)」记录强引用数和弱引用数。

栈                  堆
                    ┌──────────────────────────┐
 a ──────────────▶  │ strong: 2   weak: 0      │  ← RcBox 控制块
 b ──────────────▶  ├──────────────────────────┤
                    │ 数据:String("payload")  │
                    └──────────────────────────┘
rust
use std::rc::Rc;

fn main() {
    let a = Rc::new(String::from("payload"));
    println!("a 之后 strong = {}", Rc::strong_count(&a)); // 1

    let b = Rc::clone(&a); // 计数 +1,不复制字符串
    println!("clone 后 strong = {}", Rc::strong_count(&a)); // 2
    println!("两个引用看到同一份数据:{a} / {b}");
    println!("指针相同 = {}", Rc::ptr_eq(&a, &b)); // true
    println!("地址相同 = {}", std::ptr::eq(&*a, &*b)); // true

    drop(b);
    println!("drop b 后 strong = {}", Rc::strong_count(&a)); // 1
}

输出:

a 之后 strong = 1
clone 后 strong = 2
两个引用看到同一份数据:payload / payload
指针相同 = true
地址相同 = true
drop b 后 strong = 1

💡 对照Rc::clone(&a) 看起来像深拷贝,其实只是 strong += 1。写 a.clone() 也对, 但社区惯例是写 Rc::clone(&a)——为了在代码里一眼看出「这里是加计数,不是拷贝数据」。 这是 Clippy 不会报但 reviewer 会在意的可读性约定。

为什么 Rc 只读

Rc<T> 只实现了 Deref没有实现 DerefMut

rust
use std::rc::Rc;

fn main() {
    let a = Rc::new(String::from("hi"));
    let b = Rc::clone(&a);
    a.push_str(" there"); // ❌ 需要 &mut String,但 Rc 只给 &String
    println!("{b}");
}

编译器会告诉你为什么:

error[E0596]: cannot borrow data in an `Rc` as mutable
 --> rc_mut.rs:6:5
  |
6 |     a.push_str(" there"); // ❌ 需要 &mut String,但 Rc 只给 &String
  |     ^ cannot borrow as mutable
  |
  = help: trait `DerefMut` is required to modify through a dereference,
    but it is not implemented for `Rc<String>`

原因很实在:如果 Rc 允许可变解引用,那 ab 就同时拥有了「可变访问同一份数据」的 能力,〈所有权〉一章的借用规则(要么多个只读、要么一个独占)就被彻底破坏了。 想在共享的前提下修改,必须把「借用的检查」推迟到运行期——这正是 「内部可变性:CellRefCell」一节的 RefCell

⚠️ 陷阱*a 读没问题(Deref 给了 &T),但任何需要 &mut T 的操作 (a.push_str(..))都会报 E0596*a = new_value 则报 E0594——两者都是 「Rc 没实现 DerefMut」。这是设计上的安全,不要用 unsafe 绕过。

T 必须是 Sized(但可以是 [T] / str

Rc<T>T: ?Sized 是针对 Rc<[T]> / Rc<str> 这类定长类型的;日常使用中 Rc::new(v) 要求你给的 v 是具体大小的类型。想共享一个切片:

rust
use std::rc::Rc;

fn main() {
    // 先用 Vec 造,再转成 Rc<[i32]>
    let rc_slice: Rc<[i32]> = vec![1, 2, 3].into();
    let rc2 = Rc::clone(&rc_slice);
    println!("{:?} 长度 {}", rc2, rc2.len());

    let rc_str: Rc<str> = Rc::from("shared text");
    println!("{} {}", rc_str, rc_str.len());
}

输出:

[1, 2, 3] 长度 3
shared text 11

Rc::try_unwrapRc::get_mut:取回独占权

Rc 的数据只有在强引用数为 1 时才能安全地变成独占:

rust
use std::rc::Rc;

fn main() {
    let a = Rc::new(vec![1, 2, 3]);
    let b = Rc::clone(&a);

    // 还有人共享 -> 拿不到可变引用
    println!("get_mut 在有共享时 = {:?}", Rc::get_mut(&mut a.clone()));
    // try_unwrap 在有共享时返回 Err,把 Rc 原样还给你
    match Rc::try_unwrap(b) {
        Ok(v) => println!("取回:{v:?}"),
        Err(rc) => println!("还有人用,取不回:{:?}", &*rc),
    }

    // 只剩一个强引用之后,get_mut 成功,可以零拷贝改数据
    let mut only = Rc::new(String::from("solo"));
    if let Some(s) = Rc::get_mut(&mut only) {
        s.push_str(" -> mutated");
    }
    println!("{}", only);
}

输出:

get_mut 在有共享时 = None
还有人用,取不回:[1, 2, 3]
solo -> mutated

🚀 进阶Rc::get_mut 返回 Option<&mut T>,是「用引用计数做写时复制(COW)」的 基础。想让共享转独占时只拷贝一次,就能用 Arc::make_mut(对应多线程版)。


引用循环与内存泄漏:用 Weak 打破环

Rust 允许泄漏,泄漏不是 UB

Rc 的释放靠计数归零,而互相强引用会让计数永远到不了 0:

a ──Rc──▶ b      a.strong = 2
▲          │      (a 变量 + b.next)
│          │      b.strong = 2
└──Rc──────┘      (b 变量 + a.next)

drop(a); drop(b);   → a.strong = 1, b.strong = 1,谁都不归零,两个节点永不释放
rust
use std::cell::RefCell;
use std::rc::Rc;

struct Node {
    name: &'static str,
    next: RefCell<Option<Rc<Node>>>,
}

impl Drop for Node {
    fn drop(&mut self) {
        println!("drop Node {}", self.name);
    }
}

fn main() {
    let a = Rc::new(Node {
        name: "a",
        next: RefCell::new(None),
    });
    let b = Rc::new(Node {
        name: "b",
        next: RefCell::new(None),
    });

    *a.next.borrow_mut() = Some(Rc::clone(&b)); // a 强引用 b
    *b.next.borrow_mut() = Some(Rc::clone(&a)); // b 强引用 a -> 成环

    println!("成环后:a strong = {}", Rc::strong_count(&a));
    drop(a);
    drop(b);
    println!("两个句柄都 drop 了,但 Drop 一次也没跑 —— 这就是泄漏");
}

输出:

成环后:a strong = 2
两个句柄都 drop 了,但 Drop 一次也没跑 —— 这就是泄漏

为什么这不是 unsafe:泄漏的内存不会被别人访问,没有悬垂指针、没有数据竞争, 所有安全保证(内存安全、线程安全)都还成立——只是这块内存永远不会被回收。 Rust 的承诺是「安全」,不是「不泄漏」。极端情况下 std::mem::forget 也能泄漏, 它是安全函数(详见「std::mem 常用函数」)。真正的泄漏(比如无界缓存)是性能/资源 bug,不是 UB。

💡 对照:Python 的引用计数同样处理不了循环,所以 CPython 额外加了分代 GC 来定期回收循环垃圾。Rust 没有 GC,也不打算加——它把「怎么打破循环」交给你, 给你一个零成本的工具 Weak

Weak<T> 打破环

Weak<T>弱引用:它指向同一份数据但不增加强引用数,因此不阻止数据释放。 访问时必须先 upgrade() 升级成 Rc<T>,如果数据已经释放就返回 None

 a(父)────Rc────▶ b(子)            Rc:进入数据需要它归零

                    └──Weak──▶ a       Weak:指向控制块,但不阻止释放

数据结构死亡顺序:强引用数归零 -> 数据析构 -> 强引用数 0 时控制块整体释放
(Weak 存活期间控制块必须保留,这就是 weak 计数存在的意义)

upgrade / downgrade 是两个方向的转换:

操作方向作用
Rc::downgrade(&rc)Rc<T>Weak<T>生成弱引用,weak_count += 1
weak.upgrade()Weak<T>Option<Rc<T>>成功则 strong_count += 1,失败返回 None

父子树的标准写法:父节点用 Vec<Rc<Node>> 强引用子节点(拥有), 子节点用 Weak<Node> 弱引用父节点(不拥有)。这不会有环:所有权是单向的。

rust
use std::cell::RefCell;
use std::rc::{Rc, Weak};

struct Node {
    name: String,
    parent: RefCell<Weak<Node>>,          // 子 -> 父:弱引用
    children: RefCell<Vec<Rc<Node>>>,     // 父 -> 子:强引用
}

impl Node {
    fn new(name: &str) -> Rc<Node> {
        Rc::new(Node {
            name: name.to_string(),
            parent: RefCell::new(Weak::new()), // Weak::new() 是一个「空」弱引用
            children: RefCell::new(Vec::new()),
        })
    }

    fn add_child(parent: &Rc<Node>, child: &Rc<Node>) {
        *child.parent.borrow_mut() = Rc::downgrade(parent);
        parent.children.borrow_mut().push(Rc::clone(child));
    }
}

fn main() {
    let root = Node::new("root");
    let leaf = Node::new("leaf");
    Node::add_child(&root, &leaf);

    // leaf 的强引用:局部变量一份 + root.children 里一份 = 2
    // leaf 的弱引用:没人弱引用它 = 0
    println!(
        "leaf: strong = {}, weak = {}",
        Rc::strong_count(&leaf),
        Rc::weak_count(&leaf)
    );

    // 自下而上:upgrade 拿到父节点
    let up: Option<Rc<Node>> = leaf.parent.borrow().upgrade();
    println!("leaf.parent = {}", up.as_ref().map_or("(已释放)", |p| &p.name));

    // root 的强引用只有局部变量一份 = 1;
    // 弱引用是 leaf.parent 里的那份 = 1
    println!(
        "root: strong = {}, weak = {}",
        Rc::strong_count(&root),
        Rc::weak_count(&root)
    );
} // root 和 leaf 都离开作用域 -> strong 归零 -> 两者都正常析构,无泄漏

输出:

leaf: strong = 2, weak = 0
leaf.parent = root
root: strong = 2, weak = 1

验证无泄漏:给 Weak 版的 Node 加上 Drop,然后运行下面的完整程序, 就能看到 rootleaf 都被正常析构:

rust
use std::cell::RefCell;
use std::rc::{Rc, Weak};

struct Node {
    name: String,
    parent: RefCell<Weak<Node>>,
    children: RefCell<Vec<Rc<Node>>>,
}

impl Node {
    fn new(name: &str) -> Rc<Node> {
        Rc::new(Node {
            name: name.to_string(),
            parent: RefCell::new(Weak::new()),
            children: RefCell::new(Vec::new()),
        })
    }

    fn add_child(parent: &Rc<Node>, child: &Rc<Node>) {
        *child.parent.borrow_mut() = Rc::downgrade(parent);
        parent.children.borrow_mut().push(Rc::clone(child));
    }
}

impl Drop for Node {
    fn drop(&mut self) {
        println!("drop {}", self.name);
    }
}

fn main() {
    let root = Node::new("root");
    let leaf = Node::new("leaf");
    Node::add_child(&root, &leaf);
    println!("构建完成,main 即将结束");
}

输出:

构建完成,main 即将结束
drop root
drop leaf

注意释放顺序:局部变量按声明逆序释放,所以 leaf 的句柄先 drop(计数从 2 降到 1), 然后 root 的句柄 drop(计数归零)→ rootDrop::drop 执行 → 编译器接着释放 root 的字段 → children: Vec<Rc<Node>> 里那份 leaf 的引用被 drop,leaf 计数归零 → leafDrop::drop 执行。 两行 drop 都出现,就是「没有泄漏」的证据;如果 parent 用的是 Rc 而不是 Weak, 这里一行都不会打印。

⚠️ 陷阱Weakupgrade() 结果是 Option<Rc<T>>必须处理 None (数据已被释放)。不要在 upgrade() 拿到 Rc 后把它存进会成环的地方,否则又变回强引用环。 常见错误是 self.parent.borrow().upgrade().unwrap()——父节点已释放时会 panic。

🧠 原理Weak 的计数(weak_count)保证控制块活到最后一个弱引用消失。 强引用先归零 → 数据(T)被析构 → 控制块保留到 weak_count == 0 才整体释放。 所以 upgrade() 总是安全的:要么数据还在,要么控制块明确告诉你「已经没了」。

💡 对照:C++ 的 weak_ptr::lock()Weak::upgrade() 语义一致。 区别是 C++ 需要手动 shared_ptr / weak_ptr 配对,漏写就循环泄漏; Rust 里 Weak 必须显式 upgrade 才能用,忘不掉。


延伸阅读


内容以 rustc 1.98.1 · Rust 2024 edition 为基准