【Rust自学】15.1. 使用Box<T>智能指针来指向堆内存上的数据
喜欢的话别忘了点赞、收藏加关注哦(加关注即可阅读全文),对接下来的教程有兴趣的可以关注专栏。谢谢喵!(=・ω・=)
15.1.1. Box<T>
box<T>
可以被简单地理解为装箱,它是最简单的智能指针,允许你在堆内存上存储数据(而不是栈内存)。
具体的实现方式是Box<T>
在栈内存上有一小块内存,存放指针,指向它存在堆内存上的数据。也就是说,实际的数据是存储在堆内存上的。除了它把数据存在堆内存上之外,就没有其它开销了,代价就是没有其它额外的功能。
这样看Box<T>
跟普通指针好像没什么区别,但其真正的不同是Box<T>
实现了Deref
和Drop
这两个trait。
15.1.2. Box<T>
的常见场景
在编译时,某类型的大小无法确定。但使用该类型时,上下文却需要知道它确切的大小,这个时候就可以选用Box<T>
。
当你有大量数据,想移交所有权,但需要确保在操作时不会被复制。
使用某个值时,你只关心它是否实现了特定的trait,而不关心的具体类型。
15.1.3. 使用Box<T>
在堆内存上存储数据
看个例子:
fn main() {
let b = Box::new(5);
println!("b = {b}");
}
我们将变量b
定义为具有指向值5的Box
的值,该值分配在堆上。该程序将打印b = 5
。
和其他任何拥有所有权的值一样,b
这个变量离开作用域的时候(也就是第4行花括号结束的时候),会和其他任何拥有所有权的变量一样释放内存(堆上的和栈上的都会被释放)。
15.1.4. 使用Box
赋能递归类型
在编译时,Rust需要知道一个类型所占的空间大小。但是有一种被称为递归的类型,它的大小无法在编译时确定。
以这个图为例,Cons
类型里面有两个字段,一个字段是i32
,另一个字段是这个字段Cons
本身的类型。
在编译时,Rust需要知道它的大小,i32
大小是固定的,但是Cons
的第二个字段Cons
本身的类型大小无法确定。
针对这种情况,可以使用Box
。针对递归类型,Box
有办法确定其大小。
这种东西在函数式语言中是存在的,叫做Cons List
。
15.1.5. 关于Cons List
Cons List
是来自Lisp语言的一种数据结构,这种数据结构里每个成员由两个元素组成,一个是当前项的值,比如上图中的i32
;另一个是下一个元素。
这种数据结构就这样一直递归下去直到最后一个元素(最后一个成员),它里面只包含一个Nil
值,没有下一个元素了,而Nil
值就相当于是一个终止的标记。
Nil
和None
的概念不一样,None
表示的是无效或缺失的值,而Nil
是一个终止的标记。
Cons List
由上图就可以看出是一种链表。
15.1.6. Cons List
在Rust中的替代者
Cons List
并不是Rust中的常用集合。通常情况下,Vec<T>
是更好的选择。
下面用Vec<T>
创建一个同上图结构相同的Cons List
:
enum List {
Cons(i32, List),
Nil,
}
List
这个枚举类型有两个变体:一个Cons
一个Nil
。Cons
变体附带了两个数据,一个是i32
类型,一个是List
类型。
这么写逻辑上没问题,但运行时会报错:
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
error[E0072]: recursive type `List` has infinite size
--> src/main.rs:1:1
|
1 | enum List {
| ^^^^^^^^^
2 | Cons(i32, List),
| ---- recursive without indirection
|
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
|
2 | Cons(i32, Box<List>),
| ++++ +
error[E0391]: cycle detected when computing when `List` needs drop
--> src/main.rs:1:1
|
1 | enum List {
| ^^^^^^^^^
|
= note: ...which immediately requires computing when `List` needs drop again
= note: cycle used when computing whether `List` needs drop
= note: see https://rustc-dev-guide.rust-lang.org/overview.html#queries and https://rustc-dev-guide.rust-lang.org/query.html for more information
Some errors have detailed explanations: E0072, E0391.
For more information about an error, try `rustc --explain E0072`.
error: could not compile `cons-list` (bin "cons-list") due to 2 previous errors
因为Rust需要知道类型所占的空间大小,但递归类型的大小Rust无法计算。
15.1.7. Rust计算类型所占空间大小的方法
先看看Rust是如何计算出类型所占的空间大小的。举个例子:
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
为了确定为Message
值分配多少空间,Rust 会遍历每个变体以查看哪个变体需要最多空间。
Rust 认为Message::Quit
不需要任何空间, Message::Move
需要足够的空间来存储两个i32
值,依此类推。因为每个时刻只有一种变体存在,因此Message
值所需的最大空间就是存储其最大变体所需的空间,也就是ChangeColor
这个变体。
15.1.8. 使用Box<T>
来获得确定大小的递归类型
刚才讲了,Rust需要知道类型所占的空间大小,但递归类型的大小Rust无法计算。那么只要使用确定大小的类型就可以了,而Box<T>
正好满足需求:它不存储数据,而是存储指向数据的指针,指针的大小是固定的usize
。
Rust知道Box<T>
的大小是因为Box<T>
本质上是一个指针,指针不直接存储值,所以不论指针指向的数据如何变指针本身的大小都不会变。也就是说,指针的大小不会基于它指向的数据的大小变化而变化。
针对这点就可以对原来的代码进行修改了。具体来说,把大小不确定的部分,也就是嵌套的List
类型改成Box<List>
类型:
enum List {
Cons(i32, Box<List>),
Nil,
}
这仍旧是递归,但是不会直接存储List
类型,而是以间接的方式指向堆内存中List
的位置,属于是曲线救国。
15.1.9. Box
类型的特性总结
- 只提供了“间接”存储和堆内存分配的功能
- 没有额外功能
- 没有性能开销
- 适用于需要“间接”存储的场景,例如
Cons List
- 实现了
Deref
和Drop
trait