C15 智能指针
https://doc.rust-lang.org/book/ch15-00-smart-pointers.html
指针是一种通用概念, 指的是这一类变量, 它们的值存储了内存地址, 而这个内存地址 "指向" 其他数据(即其他数据所在的内存地址). 而最常见的指针就是前面提到的引用(通过 &
表示), 它们没有额外开销, 而功能仅仅是指向另外的数据.
而 智能指针 指的是一种数据结构, 它类似指针的作用, 但有额外的附加功能(通过 其存储的元数据和对应能力).
在 Rust 标准库中有许多智能指针, 包括引用计数指针 Rc
, 原子引用计数指针 Arc
, String
, Vec<T>
等. 智能指针通常使用 struct
实现, 且通常都实现了 Deref
和 Drop
trait.
Deref
让智能指针使用时和使用引用方式一样, 而无需显式进行引用获取操作.
Drop
让我们可以自定义智能指针在出作用域后的行为.
由于智能指针种类众多, 且各种库中都包含自己实现的智能指针, 因此本章只挑选标准库中几个典型的来看:
Box<T>
: 用于在堆上分配内存Rc<T>
: 用于进行引用计数, 从而实现多 ownershipRefer<T>
和RefMut<T>
: 通过RefCell<T>
进行访问, 让运行时的 borrow check 成为可能.
此外, 本章还会讲 内部可变(interior mutability)
模式, 这种模式为一个不可变类型提供改变其内部值的接口. 同时本章还会讲在使用引用计数时的一个典型问题: 循环引用, 以及如何避免循环引用所造成的内存泄露.
使用 Box<T>
分配堆内存
Box 用于将数据存储到堆上, 创建后, 获得一个栈上智能指针对象(内部除数据指针外, 还有其他元数据). 除分配堆内存外, Box 无额外开销. 它的典型使用场景如下:
- 当有一个类型的大小无法在编译时预先知道.
- 当有大量数据不希望被复制, 只是传递所有权时
- 当表示一类只关心其实现何种 trait 类型的数据时(trait object)
Box 的简单使用如下所示:
let b = Box::new(5); // Box<i32>
println!("b={b}"); // 5
Box 的一个使用场景: 递归类型
当一个类型中包含其自身类型作为成员时, 这样的数据类型称为递归类型. 当在 Rust 中使用递归类型时, 编译时无法获知其具体占用内存大小(无法在栈上分配, 理论上递归类型可以无限自包含). 下面来看一个具体例子: con list.
con list 抽象表示如下:
(1, (2, (3, Nil)))
即它是一个包含 Value 和 Next 成员的结构体, 而 Next 成员可以没有, 也可以是 con list 类型. 它即是单链表的一种形式.
要在 Rust 中建立这种数据类型, 如果没有 Box, 如下的实现是无法通过编译的:
enum List {
Cons(i32, List),
Nil,
}
无法编译的原因是: 此类型没有一个确定的大小, 在栈上无法分配, 从而也就无法编译(编译器提示 "has infinite size").
这里引入 Box 的话, 则编译器可以知道该类型存储的确切大小(Box 类型栈上仅分配对应该结构体大小数据), 栈上值中包含头指针, 头指针指向的数据在堆上, 运行时分配内存:
enum List {
Cons(i32, Box<List>),
Nil,
}
// ...
// |1,->|2,->|3,Nil| 可以看到, 头指针在栈上, 后续指向的都是Box.
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Nil)))));
通过 Deref
将智能指针作为普通引用使用(自定义解引用 *
操作行为)
https://doc.rust-lang.org/book/ch15-02-deref.html
通过实现 Deref
trait, 可以自定义 *
(解引用操作符)的行为. 同时, 当为某个智能指针实现 Deref
后, 即可将智能指针直接作为普通引用使用, Rust 会自动对其进行内部处理.
下面首先来看 Deref
在普通引用上实现的方式, 然后通过实现一个和 Box<T>
类型类似的自定义类型, 并进行 Deref
实现, 说明 *
解引用操作符为什么无法在这类类型上像在引用类型上那样工作. 而后说明为什么 Deref
trait 能够让智能指针像引用那样工作. 最后我们会看 Rust 的 deref coercion 功能(让我们在引用和智能指针上都可以进行相同形式的操作).