Rust:智能指针 Box & Rc & Cow
RAII可以说是现代非GC语言的核心内存管理方式,而智能指针可以说是RAII的嫡长子。Rust提供了非常多样的智能指针,在智能指针之外,也有大量类型使用了RAII机制,本博客讲解Box<T>和Rc<T>两种智能指针。
注:本文所有源码来自1.90.0版本
Box
Box是Rust最常见的智能指针,它设计简单,也因此比其它指针更加高效,当然功能性也会比较单一。
Box将一份数据开在堆区,并持有它的唯一所有权,当Box销毁则堆区的数据一起销毁。
常用接口
Box用法非常简单,通过Box<T>::new就可以将一个类型放在堆上,此外还有一些可能遇到的用法。
leak:释放所有权但不释放内存
rust
pub fn leak<'a>(b: Self) -> &'a mut T
where
A: 'a,
leak是一个比较特别的接口,因为它刻意造成了内存泄露。
查看函数签名,它接受一个Self,直接返回了&mut T可变借用。返回借用的生命周期为'a,与返回值和堆分配器有关,借用的生命周期不能长于堆分配器。
它往往用于构造'static生命周期的数据。想象一下,在堆区有这样一块内存,它不会被释放,只要程序存活它也就存活,这不就是static生命周期吗?这就是leak的作用,将一个堆区内存的借用返回,这个借用会一直有效,因为堆区的数据已经没有人去回收了。
into_raw:转换为裸指针,放弃所有权
rust
fn into_raw(b: Self) -> *mut T
这是一个不太安全的操作,它让Box主动放弃所有权,并且返回一个原生指针。这一般用于某一些只能接受原生指针的场景。
from_raw:把裸指针重新包装为安全Box
rust
unsafe fn from_raw(raw: *mut T) -> Self
这个方法和刚才相反,将一个原生指针包装为Box。但是这是一个unsafe方法,因为最后Box会释放T的内存,如果它指向的内存不是来自于堆分配器,就会发生错误,这是不安全的。必须保证传入的指针原本就是从Box拆出来的。
into_inner:拿到内部的值
rust
fn into_inner(boxed: Self) -> T
这个方法直接返回Box内部的T,返回值是一个值上下文。因此如果T实现了Copy就拷贝,如果没有实现则移动。那么这个方法就可能导致Box内部的所有权转移而整体失效,后续不能再通过Box访问数据。
结构
接下来看看它是如何实现的,定义如下:
rust
pub struct Box<
T: ?Sized,
#[unstable(feature = "allocator_api", issue = "32838")] A: Allocator = Global,
>(Unique<T>, A);
pub struct Unique<T: PointeeSized> {
pointer: NonNull<T>,
_marker: PhantomData<T>,
}
pub struct NonNull<T: PointeeSized> {
pointer: *const T,
}
它的结构分为三层,首先Box本身是一个元组结构体,T是存储的数据,A是指定的内存分配器,默认使用全局Global。
第二层是Uniuqe<T>,它内部是NonNull<T>,再内层则是一个*const T。也就是说最后的数据是用*const指针存储的,这也决定了Box<T>对于T是协变的。
既然已经保证协变了,为什么还需要一个PhantomData?此处的幻影类型,不是用于影响协变的,而是用于影响析构函数的。在复合类型析构函数中,会先调用所有字段的析构函数,最终调用自己的析构函数。此处_marker标识Unique<T>在逻辑上持有T,保证会先调用T的析构函数。
Drop
在Box生命周期结束时,自动调用drop方法释放资源,在源码中它实现如下:
rust
#[stable(feature = "rust1", since = "1.0.0")]
unsafe impl<#[may_dangle] T: ?Sized, A: Allocator> Drop for Box<T, A> {
#[inline]
fn drop(&mut self) {
// the T in the Box is dropped by the compiler before the destructor is run
let ptr = self.0;
unsafe {
let layout = Layout::for_value_raw(ptr.as_ptr());
if layout.size() != 0 {
self.1.deallocate(From::from(ptr.cast()), layout);
}
}
}
}
其实还是非常简单的,拿到self.0的指针,然后通过self.1拿到堆分配器,调用dealloc把这个堆区释放掉。
Deref & DerefMut
这个Trait是第一次见,Deref意为解引用,它允许智能指针可以模仿指针的行为,从而语法上接近指针。
实现如下:
rust
impl<T: ?Sized, A: Allocator> Deref for Box<T, A> {
type Target = T;
fn deref(&self) -> &T {
&**self
}
}
impl<T: ?Sized, A: Allocator> DerefMut for Box<T, A> {
fn deref_mut(&mut self) -> &mut T {
&mut **self
}
}
当调用*box解引用时,相当于调用*(box.deref()),因此返回的借用可以被正常处理。
如实现了Deref,那么let r = *box就相当于调用box.deref()方法获取不可变借用。而如果实现了DerefMut,*box = other就相当于调用box.deref_mut获取可变借用,看起来就和指针一样。
Deref返回的表达式&**self其实有一些蹊跷,在*box时,deref方法内部拿到的是&self,第一次*self相当于拿到了box本身,也就是*self等效于box。
奇怪的地方来了,**self那不就是*box吗?*box难道不会递归调用自己的deref方法吗?这是一个死递归。
实际上,在Box内部解引用自己,并不是调用的deref方法。而是相当于直接拿到内部的T,这是对智能指针的特殊处理。因此**self相当于T,最后&**self就是&T得到不可变借用。可变借用同理。
Rc
Box是独占所有权的指针,保证堆区数据只被一个变量所有。如果需要多个智能指针指向共享一个数据,就需要Rc。
Rc通过维护计数器来统计当前有多少个指针指向同一份数据,每当一个Rc离开生命周期,drop时减少一个计数。当计数为0,说明不存在任何Rc指向该数据,就可以销毁堆区的数据了。
常用接口
- new :创建一个
Rc,计数器初始化为1
rust
fn new(value: T) -> Rc<T>
- clone :拷贝一个
Rc,计数器自增
rust
fn clone(&self) -> Self
调用clone方法会产生一个新的Rc,但是内部的数据不会发生拷贝,多个Rc指向同一个数据。
- strong_count :获取计数器当前数值,即当前有几个
Rc指向同一数据
rust
fn strong_count(this: &Self) -> usize
注意这个函数第一个参数不是self,因此必须通过Rc::strong_count进行调用。
写个例子使用以上三个接口:
rust
fn main() {
let a = Rc::new(String::from("hello"));
{
let b = a.clone(); // count += 1
println!("{}", Rc::strong_count(&a));
} // count -= 1
println!("{}", Rc::strong_count(&a));
}
首先通过Rc::new创建一个字符串,内部作用域b = a.clone()对Rc进行了拷贝,但是指向的还是同一份数据。最后在作用域内外分别输出计数器的数值,输出值为2和1。
Weak
为什么借用计数要交strong_count,难道还有一个weak_count吗?没错,Rc有可能导致循环借用,这会导致内存泄漏,因此Rc内部维护了两套计数,分别用于强借用和弱借用。
看看以下代码,会产生什么问题:
rust
struct Node {
name: String,
next: RefCell<Option<Rc<Node>>>,
}
fn foo() {
let a = Rc::new(Node {
name: String::from("A"),
next: RefCell::new(None),
});
let b = Rc::new(Node {
name: String::from("B"),
next: RefCell::new(None),
});
// A -> B
*a.next.borrow_mut() = Some(Rc::clone(&b));
// B -> A (形成循环!)
*b.next.borrow_mut() = Some(Rc::clone(&a));
println!("a strong count = {}", Rc::strong_count(&a));
println!("b strong count = {}", Rc::strong_count(&b));
}
这是使用Rc实现的链表节点,next表示下一个节点。为了内部可变性,使用了RefCell,Option则是保证其可以进行初始化为None,否则无法开出第一个节点。
在foo函数中,首先创建了两个next为None的节点,随后修改Rc的指向,使得A->B且B->A,此时就形成了循环借用。

图中省略了多余的包装,只显示最底层的核心数据。
对于Node_A来说,它被变量a和b.next同时指向,计数器为2。对于Node_B来说,它被变量b和a.next同时指向,计数器为2。
何时这两个数据才会被销毁?假设离开函数foo的作用域,此时a和b销毁,内存布局变成下图:

此时不再有任何栈区的变量指向两份数据,但是Node_A依然被b.next指向,反之Node_B依然被a.next指向。
想要Node_A被销毁,那么计数器需要减为0,也就是b.next应该被销毁。而想要b.next销毁,就需要Node_B被销毁,那么需要计数器减为0,也就是a.next应该被销毁。而想要a.next销毁,则需要Node_A被销毁...
这是一个死循环,两者相互制约,卡着对方最后那一位计数,导致两者都无法被销毁,甚至你已经无法在栈区上找到变量去访问它们了,这就是循环借用导致的内存泄露。
但是有时候我们确实需要这种,结构体内部相互指向的结构,比如图。为了保证内存安全,Rust为Rc<T>配套提供了weak<T>,它可以和Rc指向相同的数据,但是不占用strong_count计数器。
在Rc和Weak内部,都会维护一个叫做RcInner的控制块,这个控制块内部包含strong_count、weak_count、T三个字段。也就是这个控制块维护了两个计数器和真正的数据T。
规则如下:
- 构造第一个
Rc时,初始化strong_count = 1,weak_count = 1 - 当生成新的
Rc,strong_count += 1 Rc离开作用域,strong_count -= 1- 当生成新的
Weak,weak_count += 1 Weak离开作用域,weak_count -= 1- 每当
Rc离开作用域,检查:- 如果
strong_count == 0,销毁T,但是不销毁控制块,weak_count -= 1 - 如果
weak_count == 0,销毁整个控制块
- 如果
- 每当
Weak离开作用域,检查:- 如果
strong_count != 0,什么也不做 - 如果
strong_count == 0 && weak_count == 0,销毁整个控制块(此时T已经被销毁)
- 如果
这才是完整的Rc计数机制,控制块的其中一部分是T,它由Rc单独管理,只要没有Rc了,T就会被销毁。控制块的另一部分是strong_count和weak_count,它由Rc和Weak共同管理,只有两个计数器都为0,才销毁控制块。
这有一个注意点,初始化Rc时weak_count初始化为1,这个设计是表示Rc也参与weak_count计数,但是所有Rc只占用一次计数,保证控制块不会被释放。当strong_count == 0的时候,表示最后一个Rc也已经离开了,此时weak_count额外减少一次,相当于控制块此时完全交由Weak管理。
相关接口:
- Rc::downgrade :通过
Rc获得Weak
rust
fn downgrade(this: &Self) -> Weak<T, A>
通过downgrade,可以直接拿到一个Rc对应的Weak,此时weak_count会增加。
- Weak::upgrade :将
Weak提升为Rc
rust
fn upgrade(&self) -> Option<Rc<T, A>>
弱指针Weak无法直接访问数据T,它必须升级为Rc才能访问。此时就需要用到upgrade方法,它尝试将一个Weak升级为Rc,过程中会检查strong_count,如果strong_count = 0,说明T已经被销毁了,则返回None,反之升级成功,可以通过Rc来访问数据了。
结构
接下来看看Rc和Weak内部到底是如何实现的。
RcInner
Rc和Weak内部都维护了一个控制块RcInner,结构如下:
rust
#[repr(C)]
struct RcInner<T: ?Sized> {
strong: Cell<usize>,
weak: Cell<usize>,
value: T,
}
在RcInner中,有两个计数器Strong和Weak,以及存储的数值value,计数器使用Cell实现内部可变性。
这个RcInner实现了一个Trait,叫做RcInnerPtr,如下:
rust
trait RcInnerPtr {
fn weak_ref(&self) -> &Cell<usize>;
fn strong_ref(&self) -> &Cell<usize>;
fn strong(&self) -> usize;
fn inc_strong(&self);
fn dec_strong(&self);
fn weak(&self) -> usize;
fn inc_weak(&self);
fn dec_weak(&self);
}
这个Trait中定义了八个方法,前两个需要实现者自己实现,后面六个有默认实现(我没有写出来)。
其实从方法名就可以知道这些方法的功能,比如inc_strong是增加strong计数,dec_weak是减少weak计数。
对于RcInner来说,它只要实现weak_ref和strong_ref,也就是提供自己的两个计数的借用,剩下六个方法就可以生效。
但是为什么不直接impl RcInner,非要搞一个Trait间接实现这些接口?这是因为Rc还有一个兄弟Arc,它的计数规则和Rc一样,但是内部是多线程安全版本,它也要这一套强弱计数的接口,所以把它封装为了Trait。不论是Rc还是Arc,想要使用这个计数接口,只需要提供两个方法来返回计数器的借用,实现了代码复用。
Weak
Weak源代码如下:
rust
#[stable(feature = "rc_weak", since = "1.4.0")]
#[rustc_diagnostic_item = "RcWeak"]
pub struct Weak<
T: ?Sized,
#[unstable(feature = "allocator_api", issue = "32838")] A: Allocator = Global,
> {
ptr: NonNull<RcInner<T>>,
alloc: A,
}
可以看到,Weak内部只有两个字段。ptr是指向RcInner控制块的指针,NonNull内部是*const RcInner<T>。而alloc是堆分配器。
还记得
接下来看看关键方法对ptr的操作:
- downgrade
rust
pub fn downgrade(this: &Self) -> Weak<T, A>
where
A: Clone,
{
this.inner().inc_weak();
debug_assert!(!is_dangling(this.ptr.as_ptr()));
Weak { ptr: this.ptr, alloc: this.alloc.clone() }
}
downgrade是最常用的创建Weak的方式,可以看到它内部调用了inc_weak(),这是将weak_count += 1的操作,也就是创建一个新的Weak时,弱计数器自增。
- upgrade
rust
pub fn upgrade(&self) -> Option<Rc<T, A>>
where
A: Clone,
{
let inner = self.inner()?;
if inner.strong() == 0 {
None
} else {
unsafe {
inner.inc_strong();
Some(Rc::from_inner_in(self.ptr, self.alloc.clone()))
}
}
}
ungraded通过一个Weak得到Rc。代码逻辑中,先检查strong计数,如果为0说明数据已经被释放了,返回None。反之则inc_strong增加一个强计数,并返回一个Rc。
- drop
rust
fn drop(&mut self) {
let inner = if let Some(inner) = self.inner() { inner } else { return };
inner.dec_weak();
if inner.weak() == 0 {
unsafe {
self.alloc.deallocate(self.ptr.cast(), Layout::for_value_raw(self.ptr.as_ptr()));
}
}
}
drop是RAII的核心机制,当Weak离开作用域,先调用dec_weak()减少弱计数。如果减少后为0,说明没有任何一个Rc或者Weak指向控制块,此时调用dealloc回收控制块。
可以看到,在Weak内部,确实调用了RcInner进行计数统计,不论是各种Rc和Weak之间的转换,或者等到生命周期结束,都会有计数的变化。
另外的,其实存在一个Weak::new方法,它可以创建一个空的弱指针,不指向任何数据。但是Weak里面不是一个NonNull吗?怎么可能会有空指针?
Rust在此特地注释了,NonNull<RcInner>是存在空状态的,只要内部的*const取值为usize::MAX。因为RcInner的对齐数至少为2,usize::MAX是一个奇数,指针不可能对齐到这个位置,因此是一个无效取值,就用usize::MAX来表示空指针了。
Rc
Rc源代码如下:
rust
#[doc(search_unbox)]
#[rustc_diagnostic_item = "Rc"]
#[stable(feature = "rust1", since = "1.0.0")]
#[rustc_insignificant_dtor]
pub struct Rc<
T: ?Sized,
#[unstable(feature = "allocator_api", issue = "32838")] A: Allocator = Global,
> {
ptr: NonNull<RcInner<T>>,
phantom: PhantomData<RcInner<T>>,
alloc: A,
}
Rc也维护了ptr和alloc,由于NonNull内部是原生指针,所以最后所有的Rc和Weak指向的都是同一个RcInner计数器。此外还有一个PhantomData幻影类型,表示Rc在逻辑上持有RcInner<T>。
接下来看看Rc内部的核心方法:
new
rust
pub fn new(value: T) -> Rc<T> {
unsafe {
Self::from_inner(
Box::leak(Box::new(RcInner { strong: Cell::new(1), weak: Cell::new(1), value }))
.into(),
)
}
}
new直接创建一个Rc,代码逻辑很简单,就是将strong和weak计数器都初始化为1。
如果你仔细观察,会发现它使用了Box::leak,这是之前说的让Box释放所有权但是不释放数据。也就是说,Rc没有自己封装堆区的开辟逻辑,而是借用Box完成内存开辟。
clone
rust
fn clone(&self) -> Self {
unsafe {
self.inner().inc_strong();
Self::from_inner_in(self.ptr, self.alloc.clone())
}
}
clone也很简单,调用inc_strong()增加强计数。
drop
rust
fn drop(&mut self) {
unsafe {
self.inner().dec_strong();
if self.inner().strong() == 0 {
self.drop_slow();
}
}
}
unsafe fn drop_slow(&mut self) {
let _weak = Weak { ptr: self.ptr, alloc: &self.alloc };
unsafe {
ptr::drop_in_place(&mut (*self.ptr.as_ptr()).value);
}
}
drop内部调用了一个drop_slow方法,我也粘出来了,这是我个人觉得整个Rc - Weak体系设计的最精妙的部分。
当Rc离开作用域,会调用drop释放资源。它首先调用dec_strong()减少强计数,如果强计数为0,说明已经没有任何Rc指向数据了,此时可以销毁数据value,于是调用drop_slow进行销毁。
在drop_slow中,先直接创建了一个Weak智能指针,而且是直接复用ptr,这次创建Weak没有增加weak计数器。随后调用drop_in_place,它销毁的是ptr.value,也就是只销毁了数据,但是没有销毁RcInner。
看似简单的代码,却暗含很多蹊跷!为什么创建了一个_weak变量,什么也没做?为什么创建Weak不增加弱计数?
还记得吗,当strong_count = 0的时候,需要额外将weak_count -= 1,这是为了抵消最开始Rc在new的时候初始化的weak_count = 1。为什么drop_slow没有执行dec_weak()方法?
厉害的人已经反应过来了,这里是RAII。
看似我创建了一个_weak弱指针,什么也没做。但是当_weak离开drop_slow函数会发生什么?会触发Weak::drop吧?
而Weak_drop的逻辑是,先将weak_count -= 1,然后检查weak_count和strong_count,如果都为0就销毁整个RcInner。也就是说,在Weak::drop里面完成了两件事,一个是减少weak_count,这刚好可以抵消Rc::new时初始化的那个weak_count = 1。另一个是检查是否要释放数据。
回看Rc::drop,它只执行了两个操作,dec_strong()和销毁数据value。万一这是包括Rc和Weak在内,最后一个指向数据的指针,那么Rc::drop应该还需要负责RcInner的回收吧?
但是Rc::drop没有自己去干这些活,它通过创建一个不增加计数的Weak指针,并让它基于RAII触发Weak::drop同时完成了两件事:抵消初始的weak_count = 1,以及如果是最后一个指针则回收内存。
这个看似简单古怪的代码实际上非常精彩。一定要建立一种认知:RAII是一种思想,而非单纯的内存管理方案、
最后画一张图理解整个Rc-Weak体系:

Deref
最后看一个轻松的,Rc的Deref。
rust
impl<T: ?Sized, A: Allocator> Deref for Rc<T, A> {
type Target = T;
#[inline(always)]
fn deref(&self) -> &T {
&self.inner().value
}
}
Rc只提供了Deref,解引用模仿指针行为。但是不提供DerefMut,因为Rc不是独占所有权的指针,获取可变借用可能会失败,不能直接返回&mut T,至少也要返回一个Option,但是DerefMut可不能返回option,因此没有实现。
如果需要获得可变借用,可以尝试get_mut方法:
rust
fn get_mut(this: &mut Self) -> Option<&mut T>
它返回的就是Option<&mut T>,如果只有一个strong_count独占就返回Some,反之返回None表示现在不能获取可变借用。
Cow
Cow也是一个智能指针,它定义在std::borrow::Cow中,没有前两者常用,博客还是简单讲解一下。Cow不是一个"奶牛指针",它是Copy On Write的缩写,意味写时拷贝。
设想场景,现在有一个已经存在的数据,你不能破坏它的原数据。
你希望用另外一个变量去读取它,需要拷贝这个数据吗?很明显不用,因为读取不会修改数据,你可以直接读取原数据。
你希望在这个数据基础上做一些改进,需要拷贝这个数据吗?那就必须进行一次拷贝,然后在拷贝后的数据上进行修改了,这样才不会破坏原数据。
那么,假设你现在只知道你要读取它,但是不确定未来要不要修改它,你到底是拷贝还是不拷贝呢?万一你拷贝了没修改,那不是白拷贝了?
写时拷贝解决的就是这样一个问题。写时拷贝的策略是:
- 对于一份数据,默认以只读的形式使用
- 等到需要写入数据时,才把数据拷贝一份出来,再对拷贝后的数据进行修改
这就是写时拷贝,等到写入的时候再进行拷贝。
定义如下:
rust
#[stable(feature = "rust1", since = "1.0.0")]
#[rustc_diagnostic_item = "Cow"]
pub enum Cow<'a, B: ?Sized + 'a>
where
B: ToOwned,
{
/// Borrowed data.
#[stable(feature = "rust1", since = "1.0.0")]
Borrowed(#[stable(feature = "rust1", since = "1.0.0")] &'a B),
/// Owned data.
#[stable(feature = "rust1", since = "1.0.0")]
Owned(#[stable(feature = "rust1", since = "1.0.0")] <B as ToOwned>::Owned),
}
Cow是一个枚举,存在两种形态。Borrow是借用状态,此时稚嫩读取内部的数据B。Owned是拥有状态,此时会对数据进行拷贝,后续可以自由写入这个数据。
关于内部更深入的Owned结构就不讲解了,它的重要性还是不如前两者,主要介绍接口。你只要知道它是一个枚举,存在借用和拥有两个状态即可。
由于本身就是携带参数的枚举,构造的方式也很简单,直接报目标借用作为参数传给Cow即可:
rust
fn main() {
let s1: Cow<str> = Cow::Borrowed("Hello, World!");
let s2: Cow<str> = Cow::Owned(String::from("Hello, Rust!"));
println!("s1: {}", s1);
println!("s2: {}", s2);
}
s1是借用形态的Cow,s2则是所有权形态。
常用方法:
- as_ref:返回不可变借用
- to_mut :返回可变借用,如果是
Borrow则进行拷贝 - is_borrowed :判断是否为
Borrow形态 - is_owned :判断是否为
Owned形态
后面两个检查形态的方法,需要nightly版本,目前还不稳定。
示例:
rust
struct LogString(String);
impl Clone for LogString {
fn clone(&self) -> Self {
println!("LogString 被 Clone 了!");
LogString(self.0.clone())
}
}
fn main() {
let borrowed = LogString("hello".into());
let mut cow: Cow<'_, LogString> = Cow::Borrowed(&borrowed);
// 读操作,不触发拷贝
println!("读操作:{}", cow.0);
// 第一次写入 ------ 写时拷贝触发 Clone!
println!("写操作");
cow.to_mut().0.push_str(", world");
println!("内容:{}", cow.0);
}
这一个被NewType模式包装的String,它重新实现了clone方法,在拷贝时输出"LogString 被 Clone 了!"。
随后在main中创建了一个Cow,先对它进行读操作,此时不会触发拷贝。随后进行一次to_mut,并且执行写操作,此时会触发clone输出。
输出结果:
rust
读操作:hello
写操作
LogString 被 Clone 了!
内容:hello, world
观察结果,确实是第一次写入时触发的拷贝操作。
Cow也支持Deref,可以直接访问,比如刚才main内部的cow.0就是触发了隐式Deref,因此它也属于智能指针一员。