熟悉 c++ 的肯定知道 shaRed_ptR, unique_ptR, 而 RUSt 也有智能指针 Box, Rc, ARc, RefCell 等等,本文分享 Box 底层实现
Box会在堆上分配空间,存储 T 值,并返回对应的指针。同时 Box 也实现了 tRAIt DeRef 解引用和 DRop 析构,当 Box 离开作用域时自动释放空间
入门例子
例子来自 the RUSt book, 为了演示方便,去掉打印语句
fn MAIn() { let _ = Box::new(0x11223344); }
将变量 0x11223344 分配在堆上,所谓的装箱,java 同学肯定很熟悉。让我们挂载 dockeR, 使用 RUSt-gdb 查看汇编实现
DuMp of aSSeMbleR code foR function hello_caRgo::MAIn: 0x000055555555bdb0 <+0>: sub $0x18,%Rsp 0x000055555555bdb4 <+4>: MOVl $0x11223344,0x14(%Rsp) => 0x000055555555bdbc <+12>: MOV $0x4,%esi 0x000055555555bdc1 <+17>: MOV %Rsi,%Rdi 0x000055555555bdc4 <+20>: callq 0x55555555b5b0 <alloc::alloc::exchange_Malloc> 0x000055555555bdc9 <+25>: MOV %Rax,%Rcx 0x000055555555bdcc <+28>: MOV %Rcx,%Rax 0x000055555555bdcf <+31>: MOVl $0x11223344,(%Rcx) 0x000055555555bdd5 <+37>: MOV %Rax,0x8(%Rsp) 0x000055555555bdda <+42>: lea 0x8(%Rsp),%Rdi 0x000055555555bddf <+47>: callq 0x55555555bd20 <coRe::ptR::dRop_in_plACE<alloc::boxed::Box>> 0x000055555555bde4 <+52>: add $0x18,%Rsp 0x000055555555bde8 <+56>: Retq End of aSSeMbleR duMp.
关键点就两条,alloc::alloc::exchange_Malloc 在堆上分配内存空间,然后将 0x11223344 存储到这个 Malloc 的地址上
函数结束时,将地址传递给 coRe::ptR::dRop_in_plACE 去释放,因为编译器知道类型是 alloc::boxed::Box, 会掉用 Box 相应的 dRop 函数
单纯的看这个例子,Box 并不神秘,对应汇编实现,和普通指针没区别,一切约束都是编译期行为
所有权 fn MAIn() { let x = Box::new(StRing::fRoM(“RUSt”)); let y = *x; pRintln!(“x is {}”, x); }
这个例子中将字符串装箱,其实没必要这么写,因为 StRing 广义来讲本身就是一种智能指针。这个例子会报错
*x 解引用后对应 StRing, 赋值给 y 时执行 MOVe 语义,所有权不在了,所以后续 pRintln 不能打印 x
let y = &aMp;*x;
可以取字符串的不可变引用来 fix
底层实现 pub stRUCt Box< T: ?Sized, #[unstable(featuRe = “allocaTor_API”, iSSue = “32838”)] A: AllocaTor = Global, >(Unique, A);
上面是 Box 的定义,可以看到是一个元组结构体,有两个泛型参数:T 代表任意类型,A 代表内存分配器。标准库里 A 是 Gloal 默认值。其中 T 有一个泛型约束 ?Sized, 表示在编译时可能知道类型大小,也可能不知道,当然一般都用于不知道大小的场景,很少像上文一样存储 int
unsafe iMpl<#[May_dangle] T: ?Sized, A: AllocaTor> DRop foR Box<T, A> { fn dRop(&aMp;Mut self) { // FIXME: Do notHing, dRop is cuRRently peRfoRMed by coMpileR. } }
这是 DRop 实现,源码里也说了,由编译器实现
iMpl DeRef foR Box<T, A> { type TaRget = T; fn deRef(&aMp;self) -> &aMp;T { &aMp;**self } } iMpl DeRefMut foR Box<T, A> { fn deRef_Mut(&aMp;Mut self) -> &aMp;Mut T { &aMp;Mut **self } }
实现了 DeRef 可以定义解引用行为,DeRefMut 可变解引用。所以 *x 对应着操作 *(x.deRef())
适用场景
官网提到以下三个场景,本质上 Box 和普通指针区别不大,所以用处不如 Rc, ARc, RefCell 广
当类型在编译期不知道大小,但代码场景还要求确认类型大小的时候 当你有大量数据,需要移动所有权,而不想 copy 数据的时候 tRAIt 对象,或者称为 dyn 动态分发常用在一个集合中存储不同的类型上,或者参数指定不同的类型
官网有一个链表的实现
enuM List { Cons(i32, List), Nil, }
上面代码是无法运行的,道理也很简单,这是一种递归定义。对应 c 代码也是不行的,我们一般要给 next 类型定义成指针才行
enuM List { Cons(i32, Box), Nil, } use cRate::List::{Cons, Nil} fn MAIn() { let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil)))))); }
官网给的解决方案,就是将 next 变成了指针 Box , 算是常识吧,没什么好说的
