智能指针(smart pointers)是一类数据结构,String和Vec就是智能指针的例子。智能指针不同于结构体的地方在于,它实现了Deref trait和Drop trait。Deref trait允许智能指针像引用一样使用,Drop trait允许智能指针在离开作用域时自动执行一些操作。这里不会讲所有的智能指针,而是讲以下三个:
Box<T>,用在堆上分配值Rc<T>,一个引用计数类型,其数据可以有多个所有者Ref<T>和RefMut<T>,通过RefCell<T>访问
Box🔗
box是最简单直接的智能指针,它允许你将值放在堆上,而不是栈上,留在栈上的是指向堆数据的指针。
使用Box在堆上储存数据🔗
考虑下面代码:
fn main() {
let b = Box::new(5);
println!("b = {}", b);
}
这便是box的用法,这里定义了变量b,其值为5,5被放在了堆上,b是指向堆数据的指针。
Box创建递归类型🔗
递归类型(recursive type)的值可以拥有另一个同类型的值作为其自身的一部分,但是Rust在编译时需要知道类型占用了多少空间,而递归类型可以无限地递归下去,所以不知道需要多少空间。因为box有已知的大小,所以通过在循环类型定义中插入box就可以创建递归类型了。
下面探索一下一个递归类型的例子cons list,这个数据类型有两个参数,一个是值,一个是另一个cons list,最后一项包含一个叫Nil的特殊值,代表列表终止:
enum List {
Cons(i32, List),
Nil,
}
use List::{Cons, Nil};
fn main() {
let list = Cons(1, Cons(2, Cons(3, Nil)));
}
上面的代码还不能编译,因为这个类型没有已知的大小。而使用box就可以解决这个问题,因为box作为一个指针有已知的大小:
enum List {
Cons(i32, Box<List>),
Nil,
}
use List::{Cons, Nil};
fn main() {
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}使用Deref trait将智能指针当作常规引用处理🔗
实现Deref trait允许我们重载*运算符,使得智能指针可以像引用一样使用。
追踪指针的值🔗
常规引用是一个指针类型:
fn main() {
let x = 5;
let y = &x;
assert_eq!(5, x);
assert_eq!(5, *y);
}
这里创建了一个变量x,其值为5,然后创建了一个指向x的引用y,使用*y可以获取y指向的值。如果尝试编写assert_eq!(5, y);,会报错,因为不允许比较数字的引用与数字,它们是不同的类型。
像引用一样使用Box🔗
可以使用Box<T>替代引用来重写上面代码:
fn main() {
let x = 5;
let y = Box::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
这段代码与上面的代码的不同之处在于将y设置为一个指向x值拷贝的Box<T>实例。
自定义智能指针🔗
下面我们创建一个类似于标准库中Box<T>类型的智能指针:
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
这段代码定义了一个结构体MyBox<T>,它包含一个泛型类型T,是一个元组,并实现了一个名为new的方法,该方法接受一个类型为T的值,并返回一个MyBox<T>实例。
下面尝试使用MyBox<T>:
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
这段代码不能编译,因为MyBox<T>不能解引用,为了使用*运算符,需要实现Deref trait。
实现Deref trait🔗
实现Deref trait由标准库提供,要求实现名为deref的方法,该方法借用self返回一个指向内部值的引用:
use std::ops::Deref;
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
type Target = T;关联类型,这里先不做解释,deref方法返回一个指向内部值的引用,编译器就可以使用*运算符了,事实上当我们输入*y时,编译器会运行*(y.deref())。
deref返回引用的原因是如果返回值,则所有权会被移出,一般情况下我们不希望获取MyBox<T>内部值的所有权。
函数和方法的隐式Deref强制转换🔗
Deref强制转换(Deref coercions)将实现了Deref trait的类型的引用转换为另一种类型的引用。例如&String转换为&str。当这种特定类型的引用作为实参传递给和形参类型不同的函数或方法时将自动进行。
Deref强制转换使得Rust程序员编写函数和方法调用时无需增加过多显式使用&和*的引用和解引用。
下面是一个例子:
fn hello(name: &str) {
println!("Hello, {}!", name);
}
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&m);
}
这里&m的类型是&MyBox<String>,而hello函数的形参类型是&str,所以编译器会自动将&m转换为&String,然后再转换为&str。
如果没有实现Deref强制转换,则需要这样写:
fn hello(name: &str) {
println!("Hello, {}!", name);
}
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&(*m)[..]);
}
这里先使用*运算符获取MyBox<String>的值,然后使用&和[]运算符获取字符串的切片,最后将切片传递给hello函数。
Deref强制转换如何与可变性交互🔗
类似Deref trait,标准库还提供了DerefMut trait,用于在可变引用上进行Deref强制转换。
Rust会在下面三种情况时进行Deref强制转换:
- 当
T: Deref<Target=U>时,从&T到&U - 当
T: DerefMut<Target=U>时,从&mut T到&mut U - 当
T: Deref<Target=U>时,从&mut T到&URust可以将可变引用转换为不可变引用,但是不能将不可变引用转换为可变引用。
使用Drop trait运行清理代码🔗
对于智能指针第二个重要的trait是Drop trait,它允许我们在值离开作用域时运行一些代码。
Drop trait定义了一个名为drop的方法,该方法接受一个self可变引用,这里为了能看出何时调用drop方法,我们使用println!语句实现drop:
struct CustomSmartPointer {
data: String,
}
impl Drop for CustomSmartPointer {
fn drop(&mut self) {
println!("Dropping CustomSmartPointer with data `{}`!", self.data);
}
}
fn main() {
let c = CustomSmartPointer {
data: String::from("my stuff"),
};
let d = CustomSmartPointer {
data: String::from("other stuff"),
};
println!("CustomSmartPointers created.");
}
Drop trait包含在prelude中,所以无需导入,输出如下:
CustomSmartPointers created.
Dropping CustomSmartPointer with data `other stuff`!
Dropping CustomSmartPointer with data `my stuff`!
可以看到,当c和d离开作用域时,drop方法被调用,并且data字段的值也被打印出来。变量以被创建时相反的顺序被丢弃。
通过std::men::drop提前丢弃值🔗
有些时候需要提前清理某个值Rust不允许我们主动调用Drop trait的drop方法,而是可以使用std::men::drop函数,该函数接受一个值并调用其Drop trait的drop方法:
fn main() {
let c = CustomSmartPointer {
data: String::from("some data"),
};
println!("CustomSmartPointer created.");
drop(c);
println!("CustomSmartPointer dropped before the end of main.");
}
输出如下:
CustomSmartPointer created.
Dropping CustomSmartPointer with data `some data`!
CustomSmartPointer dropped before the end of main.
std::men::drop位于prelude中,所以无需导入。
ℹ️ 注意
Rust不允许我们显式调用Drop trait的drop方法,因为Rust会在main结尾对值自动调用drop方法,这会导致一个double free错误,因为Rust会尝试清理相同的值两次。