装箱是最简单直接的一种智能指针,它的类型是Box<T>
。装箱使我们可以把数据存储到堆上,并在栈上保留一个指向堆数据的指针。装箱操作常常被用于下面的场景:
- 当你拥有一个无法在编译时确定大小的类型,但又想使用这个类型的值时。
- 当你需要传递大量数据的所有权,但又不希望产生大量数据的复制行为时。
- 当你希望拥有一个实现了指定
trait
的类型值,但又不关心具体的类型时。
fn main() {let b = Box::new(5);println!("b = {}", *b + 3);println!("b = {}", b);
}
常规引用就是一种类型的指针,你可以将指针形象地理解为一个箭头,它会指向存储在别处的某个值。
装箱类似于常规指针,也可以通过解引用来获取装箱实际的值,代码中*b
就是如此。这个定义和Go
中的unsafe.Pointer
非常类似,所有具体类型的指针都可以用一种类型的指针来表示。
定义递归类型
RUST
必须在编译时知道每一种类型占据的空间大小,但有一种递归的类型却无法在编译时被确定具体大小。比如下面例子中的链表:
use crate::List::{Cons, Nil};
enum List {Cons(i32, List),Nil,
}fn main() {let list = Cons(1, Cons(2, Cons(3, Nil)));
}
我们尝试使用枚举来表达一个持有i32
值的链表数据类型,通过不断嵌套元组的形式最终组成一个列表。
但程序无法编译通过,RUST
认为这个类型拥有无限大小,无法确认类型所占用的存储空间大小。
enum Message {Quit,Move { x: i32, y: i32 },Write(String),ChangeColor(i32, i32, i32),
}
RUST
如何计算Message
类型的大小?为了计算Message
值需要多大的存储空间,RUST
会遍历枚举中的每一个成员来找到需要最大空间的那个变体。Message::Quit
不需要占用任何空间,Message::Move
需要两个存储i32
值的空间,以此类推。
因为指针大小是恒定的,要改变这样无穷递归的情况,就应该将Cons
变体中存放一个Box<T>
而不是直接存放另外一个List
值,而Box<T>
则会指向下一个List
并存储在堆上。
use crate::List::{Cons, Nil};
enum List {Cons(i32, Box<List>),Nil,
}fn main() {let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}
新的Cons
变体中需要一部分存储i32
的空间和一部分存储装箱指针的空间,这样调整之后,List
值都只需要占用一个i32
值加上一个装箱指针的空间。通过使用装箱,我们打破了无限递归的过程,进而使编译器可以计算出List
值所占用的空间。
Box<T>
属于智能指针的一种,因为它实现了Defer trait
,所以允许我们将Box<T>
的值当做引用来对待。当一个Box<T>
值离开作用域时,因为它实现了Drop trait
,所以Box<T>
指向的堆数据会自动地被清理释放。