铁锈语言之盒子
铁锈语言没有提供其它语言常见的普通指针类型,用户不能直接查看和操作内存地址。 对应地,铁锈语言提供了“智能指针”,用来安全地指引和操作保存在堆上的数据。 盒子是铁锈语言提供的最通用的“智能指针”。它在内存中的形象类似铁锈的字符串类型:
即:
- 类型本身是一个结构体,大小已知且固定,保存在栈上;
- 结构体中含有一个指针,保存了堆上某个数据的地址;
- 除了这个保存地址的指针字段,结构体中还包含了元数据字段。
实际上铁锈的字符串类型也是一个智能指针,不过它专注于承载和处理字符串。而盒子是一个 通用的 智能指针类型。
盒子的基本用法:
let b = Box::new(5);
println!("b = {}", b);
数据 5 并没有像往常一样保存在栈,而是保存在了堆上。通过盒子变量 b 即可引用该数值。 盒子变量在离开其作用域后,随即连同其指引的堆上的数据一起被自动回收。
实现递归定义
在之前的一篇 文章 中提到,枚举值不能绑定自身枚举类型。
铁锈语言的堆和栈 铁锈语言对堆和栈的使用特别在意。 栈的结构特点可以帮助铁锈语言实现对于数据 所有权 的管理,比如创建、跟踪和释放的顺序。并且,由于栈上各个数据的大小是固定已知的,用地址位移去访问栈的速度比通过层层寻址访问堆上数据更快。另外,就是内存分配,在栈上分配已知大小的内存空间更方便。在堆上分配空间,要考虑很多问题,比如寻找足够大的空间、持续维护、回收等等。 因此,铁锈语言在创建数据的时候,默认都是创建在栈上。除非程序员主动要求将数据保存在堆上,然后用合适的智能指针来指引和管理。 根据这个原则,铁锈语言要求在定义类型的时候,类型的大小必须是已知的。
如果类型是递归定义的,那么它的大小便是未知的。通过利用盒子,我们便可以将未知变为已知。下面看例子。
#[derive(Debug)]
enum MyEnum {
A(MyEnum),
B,
}
let a = MyEnum::A(MyEnum::B);
编译期错误信息:
enum MyEnum {
| ^^^^^^^^^^^ recursive type has infinite size
4 | A(MyEnum),
| ------ recursive without indirection
这样做便可以成功:
#[derive(Debug)]
enum MyEnum {
A(Box<MyEnum>),
B,
}
let a = MyEnum::A(Box::new(MyEnum::B));
println!("{:?}", a); // => "A(B)"
这样,枚举值 A 中,可以保存和取出同样枚举类型的枚举值 B。这个例子可以几乎无限地在枚举值 A 中内嵌新的枚举值 A(当然最后需要用个 B 作为最里层内嵌值结束)。
在这个枚举类型的定义中,使用盒子代替了实际的内嵌类型,从而使得整个数据类型的大小已知和固定。这样编译器就知道在创建数据的时候,如何分配空间。