Rust 核心概念(十一):智能指针
智能指针不是绕过所有权,而是把不同所有权需求编码进类型。
指针和智能指针
普通引用 &T 只是借用数据。
智能指针通常拥有数据,并附带额外能力。
例如:
Box<T>:把值放到堆上,并拥有它。Rc<T>:单线程多所有者。Arc<T>:多线程多所有者。RefCell<T>:运行时借用检查。Mutex<T>:多线程互斥访问。
不同智能指针对应不同所有权模型。
Box
Box<T> 表示在堆上分配一个值,并由 Box 拥有。
1 | |
Box 最常见用途之一是递归类型。
1 | |
如果没有 Box,递归 enum 的大小无法确定。
Box<List> 是一个指针,大小固定,所以可以编译。
类似于 C++ 中的 std::unique_ptr<T>。
Box<T> 另一个常见用途是包装 trait object。
当你只关心“它能做什么”,不关心“它到底是什么类型”时,可以把 trait object 放进 Box 里:
1 | |
dyn Draw 自身大小不固定,不能直接作为普通值使用。
Box 给它提供了一个固定大小的外壳,所以函数也常用 Box<dyn Trait> 来返回“实现同一 trait 的不同具体类型”。
Deref
智能指针通常实现 Deref。
这让它可以像引用一样使用。
1 | |
*y 会解引用 Box,拿到里面的值。
很多时候 Rust 还会自动做 deref coercion。
Drop
智能指针通常实现 Drop,用于释放资源。
1 | |
当值离开作用域时,Rust 自动调用 drop。
这就是 Rust 不需要手动释放内存的关键之一。
Rc
Rc<T> 是引用计数指针,用于单线程多所有者。
1 | |
Rc::clone 不会深拷贝字符串。
它只是增加引用计数。
当最后一个 Rc 离开作用域时,数据才会被释放。
注意:Rc<T> 只能用于单线程。
Weak
Rc<T> 通过引用计数决定什么时候释放数据。
如果两个 Rc 互相持有,就可能形成引用循环,导致计数永远不归零。
这种场景应该用 Weak<T> 表达“我能找到你,但我不拥有你”。
1 | |
Weak 不增加强引用计数,所以不会阻止数据释放。
如果数据已经释放,upgrade() 会返回 None。
简单记:
Rc:拥有数据,增加强引用计数。Weak:不拥有数据,只是弱引用。
类似于cpp中的std::weak_ptr<T>.
Arc
Arc<T> 是原子引用计数指针,可以用于多线程。
1 | |
Arc 比 Rc 成本更高,因为它的引用计数是线程安全的。
类似于cpp中的std::shared_ptr<T>.
RefCell
RefCell<T> 提供内部可变性。
它把借用检查从编译期推迟到运行时。
1 | |
borrow() 获取不可变借用。
borrow_mut() 获取可变借用。
如果运行时违反借用规则,程序会 panic。
所以 RefCell 不是取消借用规则,而是把检查时间后移。
Rc<RefCell>
Rc<RefCell<T>> 是常见组合。
它表示:
Rc:多个所有者。RefCell:内部可以修改。
1 | |
这种组合有用,但不要滥用。
它会让部分错误从编译期变成运行时。
Mutex
Mutex<T> 用于多线程共享可变状态。
1 | |
lock() 会获得锁。
返回的 guard 离开作用域时会自动释放锁。
lock() 返回 Result,因为持有锁的线程如果 panic,锁会进入 poisoned 状态。
示例里的 unwrap() 表示:如果锁被 poisoned,就让当前线程也 panic。
多线程共享时常和 Arc 一起用:
1 | |
选择规则
可以先按这个思路选择:
- 只想放到堆上:
Box<T>。 - 单线程多个所有者:
Rc<T>。 - 多线程多个所有者:
Arc<T>。 - 线程内部可变:
RefCell<T>。 - 单线程多所有者且可变:
Rc<RefCell<T>>。 - 多线程多所有者且可变:
Arc<Mutex<T>>。
延伸
Cow 和 Pin 也常和智能指针放在一起提,但它们解决的是另一类问题。
Cow
Cow 是 clone on write。
它解决的是“默认只借用,必要时才拥有或复制”的场景。
1 | |
它适合这类 API:
- 大多数时候只读。
- 只有少数分支才需要修改结果。
- 不想一开始就无条件
clone。
Pin
Pin 解决的是“值被放到某个位置后,不能再被移动”的问题。
它管的是地址稳定,不是所有权共享,也不是“能不能修改值”。
有些类型会在内部保存指向自己的指针,或者在被 poll / 访问时依赖固定地址。
一旦值被 move,这些内部地址就可能失效。
Pin 的作用就是把“已经固定住”的值标出来,让安全代码不能再把它搬来搬去。
1 | |
它最常见的形式是 Pin<Box<T>>。Box 先把值放到堆上,Pin 再保证这个值不会被安全代码移动出去。
它常见于:
async生成的 future。- 自引用结构。
- 依赖稳定内存地址的类型。
Unpin 表示这个类型即使被 pin 住,也仍然可以安全移动。
大多数普通类型都是 Unpin,所以你平时通常不会直接碰到 Pin,更多是库签名里出现它。
总结
智能指针的核心不是“指针”,而是:
用明确的类型说明这份数据到底如何被拥有、共享和修改。