4.9 KiB
Box<T>
堆对象分配
关于作者帅不帅,估计争议还挺多的,但是如果说Box<T>
是不是Rust中最常见的智能指针,那估计没有任何争议。因为Box<T>
允许你将一个值分配到堆上,然后在栈上保留一个智能指针指向堆上的数据。
之前我们在所有权章节简单讲过堆栈的概念,这里再补充一些。
Rust中的堆栈
高级语言Python/Java等往往会弱化堆栈的概念,但是要用好C/C++/Rust,就必须对堆栈有深入的了解,原因是两者的内存管理方式不同: 前者有GC垃圾回收机制, 因此无需你去关心内存的细节。
栈内存从高位地址向下增长,且栈内存是连续分配的,一般来说操作系统对栈内存的大小都有限制,因此C语言中无法创建任意长度的数组。在Rust中, main
线程的栈大小是8MB
,普通线程是2MB
,然后在函数调用时会在其中创建一个临时栈空间,调用结束后Rust会让这个栈空间里的对象自动进入Drop
流程,最后栈顶指针自动移动到上一个调用栈顶,无需程序员手动干预,因而栈内存申请和释放是非常高效的。
与栈相反,堆上内存则是从低位地址向上增长,堆内存通常只受物理内存限制,而且通常是不连续的, 因此从性能的角度看,栈往往比对堆更高。
相比其它语言,Rust堆上对象还有一个特殊之处,它们都拥有一个所有者,因此受所有权规则的限制:当赋值时,发生的是所有权的转移(只需浅拷贝栈上的引用或智能指针即可), 例如以下代码:
fn main() {
let b = foo("world");
println!("{}", b);
}
fn foo(x: &str) -> String {
let a = "Hello, ".to_string() + x;
a
}
在foo
函数中,a
是String
类型,它其实是一个智能指针结构体,该智能指针存储在函数栈中,指向堆上的字符串数据。当被从foo
函数转移给main
中的b
变量时,栈上的智能指针被复制一份赋予给b
,而底层数据无需发生改变,这样就完成了所有权从foo
函数内部到b
的转移.
堆栈的性能
很多人可能会觉得栈的性能肯定比堆高,其实未必。 由于我们在后面的性能专题会专门讲解堆栈的性能问题,因此这里就大概给出结论:
- 小型数据,在栈上的分配性能和读取性能都要比堆上高
- 中型数据,栈上分配性能高,但是读取性能和堆上并无区别,因为无法利用寄存器或CPU高速缓存,最终还是要经过一次内存寻址
- 大型数据,只建议在堆上分配和使用
总之栈的分配速度肯定比堆上快,但是读取速度往往取决于你的数据能不能放入寄存器或CPU高速缓存。 因此不要仅仅因为堆上性能不如栈这个印象,就总是优先选择栈,导致代码更复杂的实现。
Box的使用场景
由于Box
是简单的封装,除了将值存储在堆上外,并没有其它性能上的损耗。而性能和功能往往是鱼和熊掌,因此Box
相比其它智能指针,功能较为单一,可以在以下场景中使用它:
- 特意的将数据分配在堆上
- 数据较大时,又不想在转移所有权时进行数据拷贝
- 类型的大小在编译期无法确定,但是我们又需要固定大小的类型时
- 特征对象,用于说明对象实现了一个特征,而不是某个特定的类型
以上场景,我们在本章将一一讲解,后面车速较快,请系好安全带。
使用Box<T>
将数据存储在堆上
如果一个变量拥有一个数值let a = 3
, 那变量a
必然是存储在栈上的,那如果我们想要a
的值存储在堆上就需要使用Boxt<T>
:
fn main() {
let a = Box::new(3);
println!("a = {}", a); // a = 3
// 下面一行代码将报错
// let b = a + 1; // cannot add `{integer}` to `Box<{integer}>`
}
这样就可以创建一个智能指针指向了存储在堆上的5
,并且a
持有了该指针。在本章的引言中,我们提到了智能指针往往都实现了Deref
和Drop
特征,因此:
println!
可以正常打印出a
的值,是因为它隐式的调用了Deref
对智能指针a
进行了解引用- 最后一行代码
let b = a + 1
报错,是因为在表达式中,我们无法自动隐式的执行Deref
解引用操作, 你需要使用*
操作符let b = *a + 1
,来显式的进行解引用 a
持有的智能指针将在作用结束(main
函数结束)时,被释放掉,这是因为Box<T>
实现了Drop
特征
以上的例子在实际代码中其实很少会存在,因为将一个简单的值分配到堆上并没有太大的意义。将其分配在栈上,由于寄存器、CPU缓存的原因,它的性能将更好,而且代码可读性也更好。