前言
管理原始指针是充满风险的,开发人员在使用它们时需要消息处理很多细节。无意的误用会导致内存泄漏、悬置引用和双重释放(memory leaks, dangling references, and double frees)等问题。为了缓解这些问题,可以使用智能指针(smart pointer),这种方法起源自C++。
Rust中有多种智能指针。之所以称为智能,是因为还有额外的元数据和与之相关的代码,会在创建或销毁时参照和执行这些数据和代码。当智能指针超出作用域时,能够自动释放底层资源,而这是智能指针的主要用途之一。
智能指针的智能之处主要来自两个特性,称为Drop特性和Deref特性。在探索Rust中可用的智能指针类型之前,先详细了解这些特性。
Drop
这是我们多次提到的特性,可以在值超出作用域时自动释放所使用的资源。Drop特性类似于其他语言中的对象析构函数方法,它包含一个方法drop,当对象超出作用域时调用该方法。该方法以&mut self作为参数,值所占空间的释放遵循后进先出的原则。看下下面的代码:
// drop.rs
struct Character {
name: String,
}
impl Drop for Character {
fn drop(&mut self) {
println!("{} went away", self.name)
}
}
fn main() {
let steve = Character {
name: "Steve".into(),
};
let john = Character {
name: "John".into(),
};
}
编译通过,结果如下:
如果需要,drop方法是清理代码的理想工具。对于清理那些不好确定的类型,比如使用计数引用或垃圾收集器,还是很方便的。当我们实例化任何Drop实现值(任何堆分配的类型)时,Rust编译器在编译后的每个作用域结束后插入Drop方法调用。因此,不需要在这些实例上手动调用drop。这种基于作用域的自动回收机制来自于C++的RAII原理。
Deref and DerefMut
为了提供与常规指针类似的功能,即能够对所指向的底层类型上的调用方法进行引用解除(dereference),智能指针类型通常对Deref特性进行实现,这就使得开发者可以对这些类型使用*(解除引用操作符)。Deref提供只读访问,而DerefMut,可以提供对底层类型的可变引用。Deref具有以下类型签名:
pub trait Deref {
type Target: ?Sized;
fn deref(&self) -> &Self::Target;
}
上面代码定义了一个名为Deref的方法,该方法接收self的引用,并返回对底层类型的不可变引用。这里面涉及与Rust的deref强制( coercion feature)结合,从而减少了大量必须编写的代码。Deref强制是指一个类型自动从一个类型引用转换到另一个类型引用。我们将在第七章涉及高级概念时,进行讨论。
智能指针类型
标准库中的一些智能指针类型如下:
- Box<T>:提供最简单的堆分配形式,Box类型拥有内部的值,因此可以用于在结构中保存值或从函数中返回值。
- Rc<T>:用于引用计数。当某处接收一个新的引用时,它会使计数器加1,当释放一个引用时,它会使计数器减1。当计数器达到0时,该值将被删除
- Arc<T>:用于原子引用计数。这与前一种类型类似,但具有原子性,以保证多线程安全
- Cell<T>:用于实现Copy特性的类型的内部可变性。换句话说,以此可获得某对象多个可变引用的可能性。
- RefCell<T>:提供类型的内部可变性,而不需要Copy特性。安全起见,会在运行时进行锁定。
Box<T>
标准库中的泛型类型Box为开发者提供了进行堆分配的最简单方法。它只是在标准库中被声明为一个元组结构,把分配的值进行包装并将其放在堆上。Box类型的所有权语义取决于封装的类型( wrapped type)。如果基础类型是Copy,则Box实例变为Copy,否则进行默认移动。
要使用Box创建类型为T的堆分配值,只需调用关联的new方法,并传入值。创建封装类型T的Box值将返回Box实例,它是栈上指向在堆上分配T的指针。下面的示例演示如何使用Box:
// box_basics.rs
fn box_ref<T>(b: T) -> Box<T> {
let a = b;
Box::new(a)
}
struct Foo;
fn main() {
let boxed_one = Box::new(Foo);
let unboxed_one = *boxed_one;
box_ref(unboxed_one);
}
在main函数中,我们通过调用Box::new(Foo)在boxed_one中创建了一个堆分配值。Box类型可被可用于创建递归类型定义,比如,下面的Node(节点)类型表示单链表中的节点:
// recursive_type.rs
struct Node {
data: u32,
next: Option<Node>
}
fn main() {
let a = Node { data: 33, next: None };
}
编译时,会出现这样的错误:
意思说不能有这个节点类型的定义,因为next有一个指向自身的类型。如果这个定义是可以允许的,那么编译器就无法停止对节点定义的分析了,因为它会一直对节点定义求值,直到超出内存为止。下面的代码片段可以说明这点:
struct Node {
data: u32,
next: Some(Node {
data: u32,
next: Node {
data: u32,
next: ...
}
})
}
如此的话,节点定义的计算将一直进行,直到编译器耗尽内存。此外,由于每个数据在编译时都需要有一个静态已知的大小,这在Rust中是一个不可表示的类型。我们需要使下一个字段具有固定的大小,可以通过将next放在指针后面来实现这一点,因为指针总是固定大小的。如果你看到编译器错误消息,我们将使用Box类型,新节点定义更改如下:
struct Node {
data: u32,
next: Option<Box<Node>>
}
可以编译通过,读者可以自行测试。
当定义需要隐藏在Sized indirection的递归类型时,也会使用Box类型。因此,由一个对自身有引用的变量组成的enum可以在以下情况下使用Box类型来隐藏该变量:
- 当需要将类型存储为trait对象时
- 当需要在集合中存储函数时
结语
本篇着重介绍了智能指针的大略,下一篇介绍一个相对具体的指针类型:引用计数智能指针(Reference counted smart pointers)。
主要参考和建议读者进一步阅读的文献
https://doc.rust-lang.org/book
深入浅出 Rust,2018,范长春
Rust编程之道,2019, 张汉东
The Complete Rust Programming Reference Guide,2019, Rahul Sharma,Vesa Kaihlavirta,Claus Matzinger
Hands-On Data Structures and Algorithms with Rust,2018,Claus Matzinger
Beginning Rust ,2018,Carlo Milanesi
Rust Cookbook,2017,Vigneshwer Dhinakaran