说实话,我刚学 Rust 时被"智能指针"这个名字骗了。以为是什么高大上的东西,结果翻开书一看——Box、RefCell 这些名字土得不行。但正是这些土名字,让我慢慢摸清了 Rust 的脾气。

内容导航


核心概念速查

  • 所有权 (Ownership): Rust中每个值有唯一所有者,当所有者离开作用域值被删除
  • 借用 (Borrowing): 通过引用访问值,不转移所有权,分为可变借用(&mut)和不可变借用(&)
  • 生命周期 (Lifetime): 引用有效的时间范围,编译器通过它确保引用不会空悬
  • 智能指针 (Smart Pointer): 实现了Deref/Drop trait的数据结构,比普通引用有更多能力

从"指针"说起

我之前没学过 C/C++,所以对"指针"这个概念完全是空白。老师说指针就是地址,我只能死记。真正开窍是在某个深夜,我突然把内存想象成了一个邮局的信箱。

每个信箱有个编号(地址),里面可以放东西(数据)。指针就是写着编号的纸条。纸条本身不值钱,关键是它指向哪个信箱。

但 Rust 的指针比我之前理解的复杂一点,它给指针加了两条规则:

  • 你可以复制纸条(Copy),但有些纸条只能撕一张(Move)
  • 你得保证纸条不会指向空信箱,否则程序会崩溃

这就是 Rust 所有权概念的开始。

智能指针是什么

普通指针只管地址,智能指针帮你管两件事:生命周期资源释放

在 Rust 里,这两个职责被封装成了两个 trait:

// 解引用,让我用 * 访问指向的值
pub trait Deref {
    type Target;
    fn deref(&self) -> &Self::Target;
}

// 析构函数,对象离开作用域时自动调用
pub trait Drop {
    fn drop(&mut self);
}

当你对一个类型实现了这两个 trait,它就是个智能指针。Box 最典型,它在离开作用域时自动释放堆内存。

Box:最老实的孩子

Box 干的事情很简单——把数据塞到堆上,然后记住它的地址。

let b = Box::new(5);
println!("{}", *b);  // 解引用拿到 5

刚学那会儿我觉得这多此一举,栈上的数据不好吗?为什么要放到堆上?

后来用多了才明白,Box 有三个正经用途:

递归类型。Rust 编译时要知道一个类型占多少空间,但链表节点包含下一个节点,下一个节点又包含再下一个——编译器卡住了。Box 打了个补丁:“这个节点的大小我知道了,里面的链表头指针占 8 字节就行”。

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

trait 对象。我想让不同类型实现同一个接口,但编译器不知道具体是哪种类型。Box 把这个问题绕过去了。

trait Draw {
    fn draw(&self);
}

fn render(obj: Box<dyn Draw>) { ... }

减少复制开销。大结构体直接传值会复制整个东西,Box 只复制指针,效率高得多。


如果你觉得这些场景还不过瘾,想看看Box在真实项目中的威力——关注公众号回复『Box』,获取我整理的Rust智能指针实战代码库


Rc:多个老板的问题

我之前一直以为 Rust 的所有权是"一把钥匙配一把锁",后来才发现 Rc 是个例外。

let a = Rc::new(String::from("hello"));
let b = a.clone();  // 引用计数 +1
let c = a.clone();  // 再 +1

Rc 的全称是 Reference Counting。它让多个变量共享同一个数据的所有权,但内部有个计数器在记录"有多少个老板"。每次 clone 计数加一,每次离开作用域计数减一,等到归零,数据就被清理掉。

关键是,Rc 只能让你读,不能改。因为它内部是 immutable 的。


不过,单线程共享不修改的场景毕竟是少数。下一节我们会看到如何’作弊’——想提前了解RefCell的黑魔法?公众号里回复『内部可变性』,我给你发一份我的学习笔记


RefCell:我有我的办法

有时候我就是想改数据,但编译器说你没有可变引用。

let x = 5;
// let y = &mut x;  // 编译错误

RefCell 杀出来了。它在运行时检查借用规则,而不是编译时。

let data = RefCell::new(5);
let r1 = data.borrow();
let r2 = data.borrow();  // runtime panic!

编译期不报错,跑起来才崩。这种玩法叫"内部可变性"——我对外面的代码说"我只读",但内部偷偷摸摸能改。

配合 Rc 使用就成了经典的组合模式:

let shared = Rc::new(RefCell::new(vec![1, 2, 3]));
shared.borrow_mut().push(4);  // 可以改

引用循环:甜蜜的烦恼

Rc 好用,但它埋了个雷——循环引用。

let a = Rc::new(Node::new("A"));
let b = Rc::new(Node::new("B"));
a.borrow_mut().next = Some(b.clone());
b.borrow_mut().next = Some(a.clone());  // 循环了!

a 引用 b,b 引用 a,引用计数永远到不了零,内存泄漏。我当时在这个坑里蹲了两天。

Weak 是解药。它不参与引用计数,属于"弱引用"。

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

let leaf = Rc::new(Node::new("leaf"));
let weak = Rc::downgrade(&leaf);

Weak 用 upgrade() 拿回 Rc,如果数据已经被释放,它返回 None。这种设计让节点可以持有对父节点的引用而不形成循环。

设计哲学

学完这一圈,我回头看 Rust 的设计思路,慢慢有点感觉了。

Rust 不相信"到时候再说"。它把能检查的错误都塞到编译期,逼你在写代码时就想清楚。RefCell 把检查推迟到运行时,已经是某种妥协——但至少你做了选择,知道自己在赌。

智能指针的设计也是这样。Box 告诉你"这数据可能会被传来传去",Rc 告诉你"这数据可能被多方持有",RefCell 告诉你"我要故意打破不可变规则"。这些不是语法糖,而是意图的声明。

现在每次用 Box 或者 Rc,我脑子里会先过一遍:这个数据谁拥有?谁能改?生命周期多长?问完这三个问题,代码自然就清晰了。

Rust 难学,但每学一个概念就少踩一个坑。这种感觉,比吃透八大设计模式还爽。


恭喜你解锁了Rust智能指针三巨头!不过这只是开始——关注公众号,回复『ARC』,下周我们讲讲多线程版本的『原子引用计数Arc』和『互斥锁Mutex』,带你进入真正的并发世界!


常见问题 (FAQ)

Rust中Box是什么?什么时候使用?

Box是将数据分配到堆上的智能指针,适用于:大数据量栈上放不下、trait对象实现多态、递归数据结构等场景。

Rc和Box的主要区别是什么?

Box是单一所有权,Rc是引用计数允许多个所有者共享只读数据,但不能修改。

RefCell如何在运行时实现可变性?

RefCell绕过了编译时的借用检查,把检查推迟到运行时。它内部维护一个锁,在任意时刻只允许一个可变借用或多个不可变借用。

什么时候使用Rc<RefCell>组合?

当需要多个所有者共享数据且其中某些所有者需要修改数据时使用,例如图结构中的节点、缓存实现等。

Rc能在线程间共享吗?

不能,Rc是非线程安全的。跨线程共享需要使用Arc(原子引用计数)。


看完玩具箱的故事,是不是觉得Rust也没那么可怕了?

欢迎关注『梦兽编程』公众号,我会持续输出Rust、AI、工具相关的干货,我们下期再见!


📖 Read this article in English: Rust Smart Pointers Explained: A Practical Guide to Box, Rc, and RefCell