智能指针
指针是一个宽泛的概念, 用于表示一个包含内存地址的变量, 这个地址指向某个数据. 在 Rust 中最常见的指针是引用, 引用会 borrow 它指向的值, 由于引用没有任何其他功能, 因此也没有额外开销.
和引用不同, 智能指针是一个数据结构, 能够实现指针的功能, 同时有额外的元数据以及能力.
Rust 标准库中提供了若干种智能指针, 比如引用计数指针, 允许一个值有多个 owner, 当所有 owner 都出作用域后, 会自动释放值. 和引用不同的是, 智能指针拥有值的所有权.
之前看到的 Vec<T>
和 String
类型内部实际都是智能指针实现. 智能指针通常使 用 struct 实现, 且一般都会实现 Deref
和 Drop
Trait. 其中 Deref
允许智能指针在使用时和引用一样. Drop
允许自定义值的释放方式.
Rust 标准库中提供了许多智能指针, 且其他的 library 也可以提供自己的智能指针, 因此下面主要介绍一些最常用的智能指针, 根据讲的内容, 后面也可以自己实现智能指针:
Box<T>
: 在堆内存上分配值Rc<T>
: 允许值有多个 owner 的引用计数指针Ref<T>
和RefMut<T>
, 通过RefCell<T>
访问, 允许在运行时而非编译时进行 borrow 规则检查.
此外, 还会讲内部可变(interior mutability)模式, 这个模式让不可变类型暴露一个修改内部值的 API. 另外在使用引用计数时还牵扯到循环引用的问题, 循环引用会导致内存泄露.
使用 Box<T>
在堆上分配内存
Box<T>
允许将数据存放到堆内存, 在栈上仅是堆上数据的指针.
Box 没有额外开销, 仅是值的存放位置不同, 在如下情况常使用 Box:
- 当有一个数据类型的大小无法在编译时确定时
- 当有大量数据需要转移所有权, 但不想将它们一一复制的时候.
- 当需要获取一个用 Trait 表示类型的数据的所有权时(而非具体类型)
针对第一种情况, 通过递归数据类型的定义来演示. 第二种情况利用复制指针而非数据的形式来极大降低复制数据量. 第三种情况即 Trait Object
的使用.
Box 的语法如下:
fn main() {
// 在堆上分配一个 i32
let b = Box::new(5);
println!("b = {}", b);
// 当 b 出作用域后, 堆内存也会被释放, 因为栈上的 b 指针已经出栈了
}
当然在堆上存放一个 i32 并不常用, 下面来看一个实际的例子, 即递归数据类型的定义.
Rust 中如果要将数据存放到栈上, 在编译时就需要知道该类型的具体大小, 但递归数据类型是不确定大小的, 因此需要借助 Box<T>
来实现, Box 指针本身是大小确定的, 只是指向的值在堆上分配.
enum List {
Cons(i32, Box<List>),
Nil,
}
use crate::List::{Cons, Nil};
fn main() {
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}
这样仅 list
在栈上存放, 且包含一个 box 指向堆上的下一个节点.
Box 实现了 Drop
, 因此当 list 出作用域后, 会按指向的链依次释放所有的结点.
Deref
Trait
通过实现 Deref
Trait, 可以自定义在该类型数据上的解引用操作符 *
行为. 通过实现 Deref
可以让智能指针像普通引用那样去使用. 比如在接收引用的地方, 也可以传入智能指针去使用.
一般情况下, 使用普通引用时:
fn main() {
let x = 5;
// 栈上 x 的引用
let y = &x;
assert_eq!(5, x);
// 必须使用解引用才能得到实际数据, 因为无法比较 i32 和 &i32
assert_eq!(5, *y);
}
和上面的例子类似, 在使用 Box 时, 我们也可以像使用普通引用那样对智能指针解引用:
fn main() {
let x = 5;
let y = Box::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
要分析为什么智能指针 Box 上可以使用解引用, 我们先来实现一个类似 Box 的数据类型 MyBox<T>
:
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
为了简化讲解, 这里没有让 MyBox 的数据在堆上分配.
如果我们在 MyBox
上使用解引用 *
, 会无法编译, 需要在 MyBox
上实现 Deref
Trait, Deref
要求实现一个 deref
方法, 接收 &self
, 且返回一个内部数据的引用:
use std::ops::Deref;
impl<T> Deref for MyBox<T> {
// 泛型协议的关联类型具体化
type Target = T;
// borrow self 并返回内部数据的引用
fn deref(&self) -> &Self::Target {
&self.0
}
}
这样实现后, MyBox<T>
就可以使用解引用操作符 *
了, 且解引用后获取的是内部的数据值.
编译器只允许普通引用的解引用, 自定义数据类型如果没有实现 Deref
, 则无法像这样解引用后获取内部数据.
在使用 MyBox<T>
并进行解引用操作时,, 由于是实现了 Deref
的, Rust 实际是这样的: *(y.deref())
, 即先在 MyBox
上调用 deref
获取数据引用后, 再解引用.
Rust 中有 Deref coercison
, 即会自动将实现了 Deref
的类型进行解引用变换为另外一个类型. 比如在需要 &str
参数的函数传参时, 我们可以传入 &String
类型的参数, 由于 String
实现了 Deref
且返回的就是 &str
, Rust 会自动调用 deref
将传入的 &String
转换为 &str
. 比如对 MyBox
类型也是同理:
fn hello(name: &str) {
println!("Hello, {name}!");
}
fn main() {
let m = MyBox::new(String::from("Rust"));
// 由于 MyBox 实现了 Deref, 当传入 &m 时, Rust 会 deref 获取内部的 &String, 再将 &String deref 得到 &str
hello(&m);
}
上面也可以看到, 自动解引用会自动进行直到获取到对应的类型, 解引用是在编译期就确定的, 因此不会在运行时有任何 开销.
试想如果 Rust 没有自动的解引用, 我们上面的代码就要写成如下, 是多冗长...:
fn main() {
let m = MyBox::new(String::from("Rust"));
// 如果 Rust 没有自动解引用的话, 就要这样写了... 即 *m 获取 Stirng, 再取得 &String, 再切片...
hello(&(*m)[..]);
}
当自动解引用需要获取可变引用 &mut
时, 需要实现 DerefMut
来重写 *
解引用行为.
Rust 会对如下三种情况的数据类型和 trait 实现情况进行自动解引用:
- 从
&T
转换到&U
: 当T
实现了Deref<Target=U>
时 - 从
&mut T
转换到&mut U
: 当T
实现了DerefMut<Target=U>
时 - 从
&mut T
转换到&U
: 当T
实现了Deref<Target=U>
时
前两个引用转换都非常好理解, 不可变 -> 不可变, 以及 可变 -> 可变.
第三个情况时, 是 可变 -> 不可变. 反向转换是不可能的, 因为有 borrow 规则的限制.
使用 Drop
Trait
当需要在类型 Owner 出作用域后自动释放, 需要实现 Drop
, 且任意类型都可以实现, 比如释放网络连接或关闭文件.
在智能指针这里介绍 Drop
是因为基本上所有智能指针都通过实现 Drop
来对值进行自动释放, 比如当 Box
出 作用域后自动释放堆内存.
编译器在遇到实现 Drop
的类型后, 会自动在合适位置插入 drop
调用.
下面是一个 CustomSmartPointer
的例子:
struct CustomSmartPointer {
data: String,
}
impl Drop for CustomSmartPointer {
// 实现自定义的 Drop, 这里仅 print 而没有其他操作
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.");
// 这里 c 和 d 出作用域后, 会自动调用 drop, 从而打印对应消息
// 且由于 c 和 d 在栈上的先后关系(此处 d 在栈顶), 因此会先打印 d 的, 再打印 c 的.
}
如果我们想手动触发 drop
调用, 则需要使用 std::mem::drop
方法, 而不能直接调用 Drop
Trait 提供的 drop
方法. Rust 不允许开发者主动调用 drop
方法, 原因是编译器仍然会自动插入 drop
调用, 手动调用后会出现 double free 的问题.
如果在某些情况下想手动触发 drop, 则需要使用 std::mem
的 drop 函数, 因为此函数是默认引入的, 因此可以直接调用而不用引入:
fn main() {
let c = CustomSmartPointer {
data: String::from("some data"),
};
println!("CustomSmartPointer created.");
// 手动调用 std::mem::drop, 它会触发实际的 drop 方法调用, 因此这里会对应打印信息
drop(c);
println!("CustomSmartPointer dropped before the end of main.");
}
两个重要协议 Deref
和 Drop
讲解完毕, 还看了 Box
的原理, 下面来看看 Rust 中其他的常用智能指针.
引用计数智能指针 Rc<T>
前面所有的内容都是在看一个值对应一个 Owner 的情况, 但有时候我们想一个值有多个 Owner, 这样可以在共享值时避免无谓的复制操作, 且有些数据结构中本质上某些值肯定有多个 Owner 的, 比如树或者是图的结点, 结点被多条边拥有, 且只有在所有边都没有后才会释放结点.
在 Rust 中这样的场景可以通过 Rc<T>
实现. 类比一家人在房间里看电视, 一个人走进来打开电视在看, 后面的人进来也可以看, 只有最后一个人走了再把电脑关上, 而不是走一个人关一次电视.
需要注意 Rc<T>
指向堆内存分配的数据, 且只能在单线程场景中使用(非线程安全).
下面是之前 List
的扩展, 使用 Rc<T>
来指向下一个结点:
enum List {
Cons(i32, Rc<List>),
Nil,
}
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));
/* 通过上述就实现了一个这样的内存数据关系:
b --- \
---> a
c --- /
*/
}
代码中 Rc::clone
仅增加引用计数, 而不是将数据克隆. 在编写代码时通常使用 Rc::clone
而非调用 Rc
的 clone
方法, 因为更可读(虽然效果都一样), 可以避免将 clone
误解为数据的深复制, 这样当出现性能问题去查找 clone
调用时, 也可以直接将 Rc::clone
跳过.
可以通过 strong_count
和 weak_count
来获取强引用和弱引用个数, 通过代码可以发现 Rc::clone
会增加引用计数, 而当 drop
被自动调用后, 会减少引用计数, 当计数到 0 时, 值被自动清理.
另外注意 Rc<T>
允许值的多个不可变引用在多个 owner 共享, 而不能对可变引用进行共享, 否则会违反 borrow 规则. 但可变数据的共享是非常重要的, 因此 Rust 提供了 RefCell<T>
, 允许在运行时检查 borrow 规则而非编译时.
RefCell<T>
和 Interior Mutability(内部可变)模式
内部可变模式允许代码中对不可变引用去改变它的内部值. Rust 正常情况下是不会允许这样做的(因为 borrow 规则限制).
在 Rust 中, 我们使用内部可变模式后, 就是对编译器说我们自己来在运行时保证 borrow 规则.
下面先来看 RefCell<T>
, 再回头总结内部可变模式.
使用 RefCell<T>
在运行时保证 borrow 规则
RefCell
对它包裹的数 据是单一所有权, 结合之前说到的 borrow 规则:
- 在任意时刻同一个值只能有一个可变引用, 或有多个不可变引用, 不能同时有可变和不可变引用
- 引用必须是有效的
结合规则对比 RefCell
和 Box
, 其中 Box 使用时仍然在编译时确保 borrow 规则, 而 RefCell 是在运行时确保 borrow 规则. 如果违反规则, Box 编译出错, 而 RefCell 的场景下是程序 panic 然后退出.
一般来说, 编译时检查是最好的选择, 可以提前暴露问题. 但某些场景下的编译时检查是不可能的, 当开发者确认 borrow 规则在运行时是正确的, 且编译器无法对这样的场景进行检查时, 就可以使用 RefCell
.
RefCell
只能用在单线程场景下(非线程安全), 且即便 RefCell
自身是不可变的, 其内部值仍然是可变的.
而内部可变模式也正是如此, 即在不可变值中改变它内部数据.
内部可变模式的例子: Mock 对象
比如我们想创建一个库来追踪某个数据的值以及和最大值还差多少(比如方法或 API 的调用次数), 接近指定阈值后发送消息:
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
LimitTracker {
messenger,
value: 0,
max,
}
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
Messenger
中有一个方法是 send
, 拿到 self
的不可变引用和一个消息 msg
. 另外一个则是 LimitTracker
的 set_value
方法, 拿到 self
的可变引用.
如果在测试方法中这样写:
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
LimitTracker {
messenger,
value: 0,
max,
}
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
struct MockMessenger {
sent_messages: Vec<String>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: vec![],
}
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
// 由于是不可变 self 中的不可变成员 sent_messages, 是无法这样用的, 会编译不过
self.sent_messages.push(String::from(message));
}
}
#[test]
fn it_sends_an_over_75_percent_warning_message() {
let mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
limit_tracker.set_value(80);
assert_eq!(mock_messenger.sent_messages.len(), 1);
}
}
上述代码中由于 send
方法中的写法, 编译不过, 因此可以使用 RefCell
:
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
LimitTracker {
messenger,
value: 0,
max,
}
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
struct MockMessenger {
// 内部可变模式的使用
sent_messages: RefCell<Vec<String>>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: RefCell::new(vec![]),
}
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
// 在不可变 self 中获取内部可变模式成员的可变引用, 这样就能正常操作了
self.sent_messages.borrow_mut().push(String::from(message));
}
}
#[test]
fn it_sends_an_over_75_percent_warning_message() {
// --snip--
let mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
limit_tracker.set_value(80);
// 通过 borrow 获取不可变引用
assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
}
}
上述代码就可以正常工作了.
RefCell<T>
可以理解为运行时的 borrow 检查, 普通引用有 &
和 &mut
, 普通引用会在编译时检查 borrow 规则, 而 RefCell
提供在运行时检查 的能力, 对应 borrow
和 borrow_mut
两个方法, 其中 borrow
返回 Ref<T>
, borrow_mut
返回 RefMut<T>
, 二者均实现了 Deref
, 因此可以像普通引用那样使用.
在运行时, RefCell
可以追踪在其上创建的 Ref
和 RefMut
数量, 比如每次创建 Ref
就会把不可变引用的个数 +1, Ref
出作用域后, 个数 -1. RefMut
也同理, 这样可以在运行时看到有多少个不可变引用和可变引用, 从而对 borrow 规则进行检查. 如果规则被违反, 则会 panic, 比如下面的代码:
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
// 违反 borrow 规则, 不能同时存在两个可变引用, 程序会崩溃
let mut one_borrow = self.sent_messages.borrow_mut();
let mut two_borrow = self.sent_messages.borrow_mut();
one_borrow.push(String::from(message));
two_borrow.push(String::from(message));
}
}
Rc
和 RefCell
结合实现多拥有者的内部可变模式
在前面讲 Rc
时就说过, 单独用 Rc
只会让一个值有多个拥有者, 每个拥有者都只能得到值的不可变访问(只读). 如果要多拥有者的可变值, 则需要结合 Rc
和 RefCell
, 比如前面的 List 例子可以改写为:
use crate::List::{Cons, Nil}; // 方便使用
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug)]
enum List {
Cons(Rc<RefCell<i32>>, Rc<List>),
Nil,
}
fn main() {
let value = Rc::new(RefCell::new(5));
let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));
let b = Cons(Rc::new(RefCell::new(3)), Rc::clone(&a));
let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a));
// 内部可变模式的多拥有者值的可变引用
*value.borrow_mut() += 10;
println!("a after = {:?}", a);
println!("b after = {:?}", b);
println!("c after = {:?}", c);
}
在某些情况下为了换取数据结构的简化实现, 可以使用 RefCell
去进行. 但要注意上述代码只适合单线程环境, 多线程下 RefCell
对应的是 Mutex
, 后续再讲.