14 智能指针
14.1 什么是指针?什么是智能指针?
指针:
- 指针(pointer)是一个通用概念,它指代那些包含内存地址的变量 常用的就是:引用
智能指针: 它和指针的的区别在于
- 指的是一些数据结构,它们的行为类似于指针但拥有额外的元数据和附加功能
- 引用是只借用数据的指针;与之相反,大多数智能指针本身就拥有它们指向的数据
对于智能指针,我们通常会为它实现,Deref Trait
和 Drop Trait
。
Deref Trait
表示能够使你同时拥有引用和智能指针的代码Drop Trait
离开函数作用域时同时销毁指向指向堆上数据的指针以及存储在堆上的数据
接下来我们讲讲几个常见的智能指针:
Box<T>
,可用于在堆上分配值Rc<T>
,允许多重所有权的引用计数类型RefCell<T>
,是一种可以在运行时而不是编译时执行借用规则的类型
14.2 使用 Box<T>
在堆上分配数据
装箱是一种简单直接的智能指针类型,它的类型被写作Box<T>
。它使我们可以将数据存储在堆上,并在栈中保留一个指向堆数据的指针。装箱常常被用于以下的场景:
- 当你拥有一个无法在编译时确定大小的类型,但又想要在一个要求固定尺寸的上下文环境中使用这个类型的值时。
- 当你需要传递大量数据的所有权,但又不希望产生大量数据的复制行为时。
- 当你希望拥有一个实现了指定trait的类型值,但又不关心具体的类型时。
第一个场景我们接下来会讲到。 在第二种场景中,转移大量数据的所有权可能会花费较多的时间,因为这些数据需要在栈上进行逐一复制。为了提高性能,你可以借助装箱将这些数据存储到堆上。通过这种方式,我们只需要在转移所有权时复制指针本身即可,而不必复制它指向的全部堆数据。
14.2.1 使用Box<T>
存储数据
我们先来看看Box
装箱的语法(代码无法通过编译),如:
let box = Box::new(5);
println!("{}", box);
我们在堆上面存储了5
这个值,然后将box
作为一个指针指向它。实际使用中,我们存储在堆上的数据不会像5
这么简单。接下来我们看一个比较复杂的例子。
14.2.1 使用装箱存储递归类型
递归类型在编译时是无法确认编译大小的,因为他们本身存储着另外一个类型的值,然后嵌套的深度也不得而知,所以Rust无法去计算出其大小。这个时候我们就可以使用装箱来创建递归类型来规避这个问题,因为装箱有固定的大小。
接下来我们使用链接列表来时创建递归类型。链接列表的每一项都包含了两个元素:当前项的值及下一项。列表中的最后一项是一个被称作Nil且不包含下一项的特殊值。
我们先来尝试使用枚举来定义一个链接列表。
enum List {
Cons(i32, List),
Nil,
}
然后使用这个List
类型来存储:
use crate::List::{Cons, Nil};
fn main() {
let list = Cons(1, Cons(2, Cons(3, Nil)));
}
这样是无法通过编译的,会提示这个类型拥有无限大小。因为Rust无法计算出存储一个List
需要花费多少的内存。
我们用装箱来重构它:
#[derive(Debug)]
enum List {
Cons(i32, Box<List>),
Nil,
}
let list = List::Cons(
1,
Box::new(List::Cons(2, Box::new(List::Cons(3, Box::new(List::Nil))))),
);
println!("{:#?}", list);
新的变体Cons
只需要一部分存储i32
类型和一部分存储装箱指针的数据空间。因此是可以Rust计算出存储大小,然后正确编译。
14.3 通过将Deref将智能指针视为常规引用
实现Deref
特征可以为我们自动实现解引用运算符的行为。然后我们可以将智能指针视作普通的引用来处理。
14.3.1 使用解引用运算符跳转到指针指向的值
我们首先来看看使用引用和解引用的例子:
let y = 5;
let x = &y;
assert_eq!(5, x); // error
assert_eq!(5, *x); // right
其中第一个比较,会发生编译报错,提示我们不能将integer
和&{integer}
进行比较。
接下来我们在来试试将Box
装箱当做常规的引用,如下:
let y = Box::new(5);
assert_eq!(5, *y);
上述的代码依然能够通过编译,说明我们的解引用操作符也能够跟踪智能指针并且获取它指向的值。
接下来我们尝试着自定义一个我们自己的智能指针。
刚刚我们有提到,需要实现Deref
才能够实现外部的解引用操作。
我们来定义一个MyBox
结构体:
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(d: T) -> MyBox<T> {
MyBox(d)
}
}
MyBox
和Box
有着相同的方法,然后我们来为它实现Deref
的行为:
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
let x = MyBox::new(5);
assert_eq!(5, *x);
我们在deref的方法体中填入了&self.0
(因为MyBox是一个元祖结构体所以.0
就能获取第一项的值),这意味着deref会返回一个指向值的引用,进而允许调用者通过*运算符
,(我们在上述的*x
会被隐式转化为*(x.deref())
)
解引用转换是Rust为函数和方法的参数提供的一种便捷特性。Rust通过实现解引用转换功能,使程序员在调用函数或方法时无须多次显式地使用&和*运算符来进行引用和解引用操作。例如:
let x = MyBox::new(String::from("world"));
fn hello(name: &str) {
println!("{}", name);
}
hello(&x);
上面的自动转化原理是:Rust先调用x的deref
方法将其转化为&String::from('world')
,然后String
内置的deref
会将其转化为字符串切片&str
,然后就能得到我们的world
值。
如果没有自动转化,那我们就需要写下面复杂的代码去获取:
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&(*m)[..]);
}
14.3.2 解引用转换与可变性
Rust会在类型与trait满足下面3种情形时执行解引用转换:
- 当T: Deref<Target=U>时,允许&T转换为&U。
- 当T: DerefMut<Target=U>时,允许&mut T转换为&mut U。
- 当T: Deref<Target=U>时,允许&mut T转换为&U。
情形三则有些微妙:Rust会将一个可变引用自动地转换为一个不可变引用。但这个过程绝对不会逆转,也就是说不可变引用永远不可能转换为可变引用。因为按照借用规则,如果存在一个可变引用,那么它就必须是唯一的引用(否则程序将无法通过编译)。将一个可变引用转换为不可变引用肯定不会破坏借用规则,但将一个不可变引用转换为可变引用则要求这个引用必须是唯一的,而借用规则无法保证这一点。
14.4 使用Drop trait 在清理时运行代码
Drop Trait 允许我们在变量离开作用域时执行自定义的操作。它常常被用来释放诸如文件、网络连接等资源。我们来实现一个拥有Drop行为的结构体,Drop trait要求实现一个接收self可变引用作为参数的drop函数, 如:
struct MyCustomPointer {
data: String,
}
impl Drop for MyCustomPointer {
fn drop(&mut self) {
println!("自定义操作")
}
}
let m = MyCustomPointer {
data: String::from("hello"),
};
println!("结束")
我们会发现执行的顺序是:
// 1.结束
// 2.自定义操作
因为是离开作用域时执行,所以drop
里面的打印会晚一些。
但是我们可以通过单独使用std::mem::drop
提前丢弃值,如下:
use std::ops::Deref;
struct MyCustomPointer {
data: String,
}
impl Drop for MyCustomPointer {
fn drop(&mut self) {
println!("自定义操作")
}
}
let m = MyCustomPointer {
data: String::from("hello"),
};
drop(m);
println!("结束")
这个时候,我们自定义的操作就能提前执行了,因为它被提前释放了。
14.5 RefCell<T>
和内部可变性模式
首先我们来了解一下什么是内部可变性模式 内部可变性是Rust的设计模式之一,它允许你在只持有不可变引用的前提下对数据进行修改
而RefCell<T>
是内部可变性模式的实践。它代表了其持有数据的唯一所有权。
想想我们之前学习的借用规则:
- 在任何给定的时间里,你要么只能拥有一个可变引用,要么只能拥有任意数量的不可变引用
- 引用总是有效的
对于使用一般引用和Box<T>
的代码,Rust会在编译阶段强制代码遵守这些借用规则。
而对于使用RefCell<T>
的代码,Rust则只会在运行时检查这些规则,并在出现违反借用规则的情况下触发panic来提前中止程序。
下面是对于Rc<T>
,Box<T>
以及RefCell<T>
使用场景:
Rc<T>
允许一份数据有多个所有者,而Box<T>
和RefCell<T>
都只有一个所有者。Box<T>
允许在编译时检查的可变或不可变借用,Rc<T>
仅允许编译时检查的不可变借用,RefCell<T>
允许运行时检查的可变或不可变借用。- 由于
RefCell<T>
允许我们在运行时检查可变借用,所以即便RefCell<T>
本身是不可变的,我们仍然能够更改其中存储的值。
14.5.1 内部可变性:可变地借用一个不可变的值
借用规则的一个推论是,你无法可变地借用一个不可变的值。 我们来写个例子看看:
let x = vec![1, 2, 3];
x.push(1); // error cannot borrow `x` as mutable, as it is not declared as mutable
x.push(1);
上面这段代码明显就违背了我们的借用规则,不能可变借用一个不可变的值。但是在某些特定情况下,我们也会需要一个值在对外保持不可变性的同时能够在方法内部修改自身。我们来写一个使用RefCell<T>
修改不可变值的例子:
use std::{cell::RefCell, mem::drop};
let x = RefCell::new(vec![1, 2, 3]);
x.borrow_mut().push(1);
println!("{:?}", x)
我们使用RefCell<T>
包裹一层我们初始化的值,然后使用borrow_mut
方法去修改原数据的值。这种场景项目中不多,但是遇到的话,我们可以保证安全性的情况下考虑使用它来解决。
14.5.2 将Rc<T>
和RefCell<T>
结合使用来实现一个拥有多重所有权的可变数据
将RefCell<T>
和Rc<T>
结合使用是一种很常见的用法。
Rc<T>
允许多个所有者持有同一数据,但只能提供针对数据的不可变访问。如果我们在Rc<T>
内存储了RefCell<T>
,那么就可以定义出拥有多个所有者且能够进行修改的值了
例如:
#[derive(Debug)]
enum List {
Cons(Rc<RefCell<i32>>, Rc<List>),
Nil,
}
use crate::List::{Cons, Nil};
use std::rc::Rc;
use std::cell::RefCell;
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(6)), Rc::clone(&a));
let c = Cons(Rc::new(RefCell::new(10)), Rc::clone(&a));
*value.borrow_mut() += 10;
println!("a after = {:?}", a);
println!("b after = {:?}", b);
println!("c after = {:?}", c);
}
打印之后,我们会发现,这三个值都发生了变化:
// a after = Cons(RefCell { value: 15 }, Nil)
// b after = Cons(RefCell { value: 6 }, Cons(RefCell { value: 15 }, Nil))
// c after = Cons(RefCell { value: 10 }, Cons(RefCell { value: 15 }, Nil))