与 cpp 类似,Rust 也有智能指针。Rust 中的智能指针与引用最大的不同是,智能指针 own 内存,而引用只是借用。
一般来讲,Rust 中的智能指针通过 struct
实现,并实现了 Deref
和 Drop
两个 trait。
Deref
允许智能指针表现得跟引用一样,因此代码可以在引用和智能指针上复用。Drop
允许自定义对象在离开作用域时的行为。
事实上,String
和 Vec
类型都是智能指针。
标准库中最常见的智能指针是:
Box
用于在 heap 上分配内存,类似于std::unique_ptr
Rc
用于引用计数(reference counting)对象,类似于std::shared_ptr
Ref
和RefMut
强制在运行时(而非编译时)执行借用规则
使用 Box 指向堆中的数据
最简单的智能指针就是 Box
,它将数据存放在堆上而非栈上。除此之外,它不存在别的 overhead,当然,也不具备其余的功能。常用的场景:
- 当不能在编译时确定对象大小时(例如递归的数据结构)
- 当希望传递 ownership 并保证不产生拷贝时
- 当 own 一个只知道 trait 而不知道具体类型的值时(例如,
Box
)
下面的例子讲了如何创建 Box
实例,以及如何使用。可以看出用法和引用没区别。
fn main() {
let num = Box::new(5);
println!("num = {}", num);
}
使用 Box
的好处是让链表一类的数据结构成为可能。如果不使用智能指针,对于这类的递归数据结构,我们无法预先知道其大小,也就无法为对象分配内存。
enum List {
Cons(i32, List),
Nil,
}
以上代码中,由于 Cons
结构体包含了 List
枚举类型,而 List
可能是另一个 Cons
。这导致无限的递归。
但是,智能指针的大小是固定的。因此可以用智能指针将这个定义改为:
enum List {
Cons(i32, Box),
Nil,
}
自己实现 Box
为了更好地理解智能指针,我们实现一个 MyBox
让他拥有跟 Box
类似的行为。
像 Box
一样,我们用一个 tuple struct 实现:
struct MyBox(T);
impl MyBox {
fn new(x: T) -> MyBox {
MyBox(x)
}
}
自定义 Deref
如果按以下代码来使用它:
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
会报错 *
操作符无法解引用。这是因为我们没有实现 Deref
trait。
use std::ops::Deref;
impl Deref for MyBox {
type Target = T;
fn deref(&self) -> &T {
return &self.0;
}
}
Rust 将 *y
替换为 *(y.deref())
,这有些类似于 cpp 中 std::unique_ptr
对 *
的重载。
Deref 的强制隐式转换
Deref
不只这么简单的解引用一个作用。
对于实现了 Deref
接口的对象,Rust 可以进行隐式转换。规则是:
一个类型为 T
的对象 foo
,如果 T
实现了 Deref
,那么,foo
的智能指针或者引用在使用时会尝试调用 deref()
直到类型匹配。
例如:
fn hello(name: &str) {
println!("Hello, {}", name)
}
fn main() {
let s = String::from("world");
let s_ptr = MyBox::new(s);
hello(&s_ptr);
}
可以看出,虽然 &s_ptr
的类型是 &MyBox
,在发现与 hello
函数类型不匹配时,转成了 &str
。具体流程是:
通过
MyBox::deref()
将&MyBox
转为了&String
,类型尚未匹配。通过
String::deref()
(String
也是智能指针类型)将&String
转为了&str
,类型匹配。
这种隐式转换可以让开发者降低负担,并且不会引入 overhead,因为都在编译时完成。
自定义 Drop
Drop
trait 可以自定义离开作用域时变量的行为,类似于析构函数。它一般用于回收资源,例如关闭文件等。Box
会回收堆上占用的内存。
我们可以为之前的结构体实现 Drop
trait,这样在销毁时可以打印信息:
impl Drop for MyBox {
fn drop(&mut self) {
println!("destroying {}", self.0);
}
}
手动调用 std::mem::drop
由于 Drop::drop
的意义是自动管理内存,Rust 不允许手动调用以避免 double free 问题。但是,标准库提供了 std::mem::drop
用于在离开作用域之前强制 drop。
由于 std::mem::drop
在 prelude 模块中,我们可以直接调用:
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
drop(y); // manually drop y here
let s = String::from("world");
hello(&s);
}
结果是:
destroying 5
Hello, world
可以看出,程序先回收了 y
再离开作用域。而且,即使我们提前回收了 y
,程序也没有因为 double free 而 crash。
使用 Rc 来共享对象
Rc
可以用引用计数的方式来共享对象。类似于 cpp 中的 std::shared_ptr
。
例如,我们需要构造如下一个链表:
由于 a
链表被 b
和 c
共享,但是在 Rust 中,我们要求了每个值只有一个 owner。因此下面的代码无法编译通过:
enum List {
Cons(i32, Box),
Nil,
}
use crate::List::{Cons, Nil};
fn main() {
let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
let b = Cons(3, Box::new(a)); // a is moved
let c = Cons(4, Box::new(a));
}
这时需要引入 Rc
智能指针了。我们将 List
中的 Cons
类型改成持有一个 Rc
:
enum List {
Cons(i32, Rc),
Nil,
}
此外,当我们需要共享 List
的实例时,我们调用 Rc::clone
来增加其引用计数:
use crate::List::{Cons, Nil};
use std::rc::Rc;
fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
let b = Cons(3, Rc::clone(&a));
let c = Cons(4, Rc::clone(&a));
println!("reference count after sharing: a = {}", Rc::strong_count(&a));
}
Rc::clone(&a)
和普通的 a.clone()
相比,由于没有做 deep copy,只是将引用计数加1,因此非常 cheap。我们使用 Rc::strong_count(&a)
可以看到 a
链表的引用计数是3,被 a
, b
, c
三个变量共同持有。类似的,在 Drop
trait 中定义了当变量离开作用域时,引用计数减少 1,减少到 0 则回收。