前言
这一篇介绍Rust中常用的智能指针以及如何自定义智能指针~
12.1 智能指针基础概念
- 指针是一种存储其它数据在内存中地址的数据类型,通过指针类型的值可以像通过索引一样找到其指向数据。
- “引用” 是Rust中最常见的指针,其具有以下特点:
- 对值使用
&
获取指针(当然根据前面的文章中可知这种说法其实不太准确,但使用引用的场景基本都离不开&
运算符); - 借用它指向的值,但不会拥有指向值的所有权;
- 没有其余开销,与其他智能指针相比的没有额外的元数据;
- 对值使用
- 智能指针指是指满足下列特点的数据结构:
- 行为和指针一样
- 有额外的元数据和功能
- Rust中引用和智能指针的区别:引用只借用数据并没有该数据的所有权,但智能指针很多时候都拥有它所指向的数据的所有权。
- 引用计数(reference counting)智能指针是一类比较常使用的智能指针类型,Rust中的引用计数智能指针类型其特点包括:
- 通过记录所有者的数量,使一份数据被多个所有者同时持有;
- 当再没有任何所有者时自动清理数据;
- Rust中使用智能指针的例子,比如说
String
和Vec<T>
,它们都拥有一下智能指针的特点:- 都拥有一片内存区域,且允许用户对其操作;
- 拥有元数据(例如容量等);
- 提供额外的功能或保障(例如String保障其数据是合法的UTF-8编码);
- Rust的标准库中就有一些常用的智能指针类型,比如:
Box<T>
:用于在堆上分配值;Rc<T>
:一个引用计数类型,其数据可以有多个所有者;Ref<T>
和RefMut<T>
:通过RefCell<T>
访问。(RefCell<T>
是一个在运行时而不是在编译时执行借用规则的类型);
12.2 常用智能指针
这里将介绍几种最基础的智能指针:
智能指针类型 | 简介 | 允许所有者数目 | 何时检查借用规则 | 谁保证借用规则通过 | 被引值是否可变 | 使用场景 |
---|---|---|---|---|---|---|
Box<T> | 最普通的智能指针 | 一个(独占所有权) | 编译期 | 编译器 | 不可变、可变 | 没要求 |
Rc<T> | 引用计数智能指针 | 多个(共享所有权) | 编译期 | 编译器 | 不可变 | 单线程 |
RefCell<T> | 内部可变性(外部不可变,内部可变) | 一个(独占所有权) | 运行时 | 开发者 | 不可变、可变 | 单线程 |
12.2.1 Box<T>
:最普通的智能指针
Box<T>
是指向类型为T
的堆内存分配值的智能指针,可以通过解引用操作符来获取Box<T>
中的T
。当Box<T>
超过作用域范围时,Rust会自动调用其析构函数,销毁内部对象,并释放所占堆内存。Box<T>
会在堆上存储数据,创建语法为Box::new(需要管理的值)
:fn main() { let x: Box<i32> = Box::new(5); let y: i32 = *x; println!("x: {}", x); // 5 println!("y: {}", y); // 5 }
Box
的功能类似C中最普通的指针,只是带上了自动释放内存的功能。- 为什么可以实现将栈上的数据变为堆上的数据:实际上
Box::new
的实现是使用了box
关键字,这个关键字曾是一个不稳定的语法,因此通过new()
函数创建Box
智能指针能更好地兼容不同版本的编译器。 Box<T>
类型是智能指针,因为它实现了:Deref
trait ,使Box<T>
值允许被当做引用对待;Drop
trait ,使Box<T>
值离开作用域时,box
(Box
里指向实际数据的字段)所指项的数据也会被清除;
例子:使用Box实现一个Lisp中的ConsList结构
use crate::List::{Cons, Nil};
fn main() {
let list = Box::new(Cons(1,Box::new(Cons(2, Box::new(Cons(3, Nil))))));
}
enum List {
Cons(i32, Box<List>),
Nil,
}
12.2.2 Rc<T>
:引用计数智能指针
- Rust提供
Rc<T>
智能指针来引用计数。 Rc<T>
允许一个值有多个所有者,引用计数确保了只要还存在所有者,该值既保持有效。每当值共享所有权时,计数会增加一。当计数为零,即所有共享变量离开作用域时,该变量才会被析构。Rc<T>
用于希望堆上分配的数据可以供程序的多个部分读取,并且无法在编译时确定哪个部分是最后使用者的场景。Rc<T>
是单线程引用计数指针,不是线程安全的类型,不允许将引用计数传递或共享给其它线程。Rc<T>
定义于std::rc
模块,所以使用前需要先use std::rc;
- 创建
Rc<T>
的时候使用Rc::new(需要管理的值)
。 - 使用
rc值.clone()
或Rc::clone(&rc值)
克隆rc值,克隆以后引用计数增加,克隆出来的rc值地址相同。 - 用法:
use std::rc::Rc; fn main() { let x = Rc::new(5); println!("{:p}: strong_count:{}", x, Rc::strong_count(&x)); // 0x7fb15ec05ae0 strong_count: 1 let y = x.clone(); println!("{:p}: strong_count:{}", y, Rc::strong_count(&x)); // 0x7fb15ec05ae0 strong_count: 2 { let z = Rc::clone(&x); println!("{:p}: strong_count:{}", z, Rc::strong_count(&x)); // 0x7fb15ec05ae0 strong_count: 3 } println!("strong_count:{}", Rc::strong_count(&x)); // strong_count: 2 }
- 当克隆值离开作用域,
Rc<T>
实例会自动计数减一,当计数为0,说明没有所有者,则清除被引用值。 - 通过不可变引用,
Rc<T>
允许在程序的多个部分之间只读地共享数据。如果Rc<T>
也允许多个可变引用,则会违反借用规则之一:相同位置的多个可变借用可能造成数据竞争和不一致。
12.2.3 RefCell<T>
:处理内部可变性场景
基础用法和说明
- 内部可变性:允许在不可变引用时也能改变数据,是Rust中的一种设计模式。使用场景:一个值不允许外部对其进行可变操作,但值内部允许进行可变操作。
- 该模式在数据结构中使用
unsafe
代码来模糊 Rust 通常的可变性和借用规则,所涉及的unsafe
代码将被封装进安全的 API 中,而外部类型仍然是不可变的;
- 该模式在数据结构中使用
- Rust的可变或不可变是针对变量绑定而言的,比如结构体的可变或不可变是指其实例的可变性,而不是某个字段的可变性。
- 实际开发中,常常需要实例不可变,但某个字段需要可变;
- 比如二叉搜索树中插入一个节点,那么就需要节点结构体实例的左子节点和右子节点是可变的;
RefCell<T>
并没有完全绕开借用规则,编译器的借用检查器允许内部可变并在运行时执行借用检查。如果在运行时出现了违反借用的规则,比如有多个可变借用会导致程序错误。RefCell<T>
通过将借用规则的检查时期延后至运行时,使修改一个不可变值的内存数据(字段)称为可能,只要修改时仍满足借用规则即可。RefCell<T>
通过引入少量的性能损耗,但是会使得代码更灵活。RefCell<T>
只适用于单线程场景。RefCell<T>
提供的borrow
方法返回Ref
类型的智能指针,borrow_mut
方法返回RefMut
类型的智能指针。use std::cell::RefCell; fn main() { let v: RefCell<Vec<i32>> = RefCell::new(vec![1,2,3,4]); println!("{:?}", v.borrow()); v.borrow_mut().push(5); println!("{:?}", v.borrow()); } // [1,2,3,4] // [1,2,3,4,5]
RefCell<T>
会记录当前有效的Ref<T>
和RefMut<T>
智能指针的数量。在任何时候,同一作用域中只允许有多个Ref<T>
或一个RefMut<T>
。use std::cell::Ref; use std::cell::RefCell; fn main() { let v: RefCell<Vec<i32>> = RefCell::new(vec![1,2,3,4]); let v_borrow_1: Ref<Vec<i32>> = v.borrow(); println!("{:?}", v_borrow_1); let v_borrow_2: Ref<Vec<i32>> = v.borrow(); println!("{:?}", v_borrow_2); } // [1,2,3,4] // [1,2,3,4]
RefCell<T>
常配合Rc<T>
来使用。Rc<T>
允许数据有多个所有者,但只能提供数据的不可变访问。如果两者结合使用,Rc<RefCell<T>>
表面上是不可变的,但利用RefCell<T>
的内部可变性可以在需要时修改数据。
Rc<T>
结合 RefCell<T>
实现值拥有多个可变引用
- 根据借用规则,一个值同一时刻要么可以有多个不可变引用,要么只能有一个可变引用;
- 为了实现一个值可以有多个可变引用,可以将
Rc<T>
和RefCell<T>
结合使用,具体做法为:Rc<RefCell<T>>
,则可以使同一份数据可以有多个RefCell<T>
引用,每一个RefCell<T>
引用又可以对改数据进行可变操作。
其它可实现内部可变性的类型
Cell<T>
:通过复制来访问数据。Mutex<T>
:用于实现跨线程情形下的内部可变性模式。
12.3 智能指针的实现
- 智能指针是一个在 Rust 经常被使用的通用设计模式,很多库都有自己的智能指针,开发者也可以编写属于自己的智能指针。
- 智能指针通常以struct的方式实现,从广义的角度看只要满足前面提到的智能指针的要求都是智能指针类型。在Rust中,为了能更好地自定义智能指针的实现,提供了
Deref
和Drop
两个trait,只要实现了这两个trait,就会被认为是智能指针,这两个trait的作用如下:Deref
trait:允许自定义智能指针的值表现得像引用一样,统一对智能指针和引用的操作。比如说指针的值为p
,则当使用*p
时将会执行Deref
中定义的方法(如果需要返回可变引用,需要实现DerefMut
trait);Drop
trait:允许开发者自定义当智能指针离开作用域时运行的代码;
12.3.1 Deref
trait:解引用
- 实现
Deref
trait的类型的值val
支持使用解引用符*
运算,即*val
(提供解引用功能)。 - 智能指针实现了
Deref
trait,且方法会返回智能指针包装的引用(只读引用)。
Box<T>
是如何实现Deref
trait的
Box<T>
是一个实现了Deref
trait的tuple struct,并重载了deref(&self) -> &Self::Target
方法。- 由于
Box<T>
实现了Deref
,当对Box
实例使用解引用运算符时,实际上Rust底层会变为对boxIns.derfer()
的结果使用解引用。即实现Deref
trait,能够自定义结构体解引用运算的目标。 deref
方法返回值的引用。 如果deref
方法直接返回值而不是值的引用,其值的所有权将被移出self
,大部分情况下这并不是开发者想要的。
Deref
trait实现示例
use std::ops::Deref;
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
fn main() {
let x= 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y); // *y 等价于 *(y.deref())
}
函数和方法的隐式解引用转换(Deref Coercion)
- 隐式解引用转换(Deref Coercion)是为函数和方法提供的一种边界特性。
- 假设
T
实现了Deref
trait:隐式解引用转换可以把T
的引用转化为T
经过deref
操作后生成的引用。 - 当把某类型的引用传递给函数或方法时,但它的类型与定义的参数类型不匹配:
- 隐式解引用转化就会自动发生;
- 编译器会对
deref
进行一系列调用,来把它转换为所需要的参数类型; - 在编译时完成,不会有额外的性能开销;
- 示例:
use std::ops::Deref; fn hello(name: &str) { println("Hello, {}", name); } fn main() { let m = MyBox::new(String::from("Rust")); // m的类型 MyBox<String> hello(&m); // 1. 由于MyBox实现了Deref,所以 &m 转化为 &String hello("Rust"); // 2. 由于String也实现了String,所以 &String 转化为 &str } struct MyBox<T>(T); impl<T> MyBox<T> { fn new(x: T) -> MyBox<T> { MyBox(x) } } impl<T> Deref for MyBox<T> { type Target = T; fn deref(&self) -> &T { &self.0 } }
DerefMut
trait
- 可使用
DerefMut
trait重载可变引用的*
运算符。 Deref
trait重载不可变引用的*
运算符。DerefMut
trait重载可变引用的*
运算符。- Rust 在发现类型和 trait 实现满足三种情况时会进行隐式解引用转换:
- 当
T: Deref<Target=U>
时从&T
到&U
。 - 当
T: DerefMut<Target=U>
时从&mut T
到&mut U
。 - 当
T: Deref<Target=U>
时从&mut T
到&U
。
- 当
12.3.2 Drop
trait:清理资源
- 实现
Drop
trait的类型的值在离开作用域时,会自动执行drop
方法。 - 智能指针实现了
Drop
trait,会在drop
方法中执行如释放内存,文件资源或网络资源关闭等操作。 - 所有权系统会保证引用总是有效的,也保证了
drop
方法只会在值不再使用时被调用一次。 - 函数中的值执行
drop
方法时,会按照变量声明顺序的反顺序执行(因为创建函数参数时需要入栈)。 Drop
trait 要求实现一个叫做drop
的方法,它获取一个self
的可变引用。这个drop
函数也叫析构参数(destructor)fn drop(&mut self) { }
Drop
位于preclude模块中。
- Rust不允许开发者显式调用
Drop
trait的drop
方法(因为会发生double free的错误),如果开发者真的要在作用域结束之前强制释放变量的话,需要使用标准库提供的std::mem::drop
。 - 示例:
struct Custom { data: String, } impl Drop for Custom { fn drop(&mut self) { println!("Dropping Custom with data: {}", self.data); } } fn main() { let str1 = Custom{data: String::from("hello world!")}; let str2 = Custom{data: String::from("hello result!")}; println!("Custom created"); println!("str1: {}", str1.data); println!("str2: {}", str2.data); // Custom created // str1: hello world! // str2: hello rust! // Dropping Custom with data: hello rust! // Dropping Custom with data: hello world! }
12.4 引用循环和内存泄漏
- 使用
Rc<T>
可能会发生引用循环,造成逻辑上的内存泄漏。 - 防止内存泄漏的解决办法。
- 为了避免引用循环,可以将
Rc<T>
改为Weak<T>
。 - 通过调用
Rc::downgrade
并传递Rc<T>
值的引用来创建其值的 弱引用(weak reference):- 调用
Rc::downgrade
时会得到Weak<T>
类型的智能指针; - 不同于将
Rc<T>
值的strong_count
加1
,调用Rc::downgrade
会将weak_count
加1
; Rc<T>
类型使用weak_count
来记录其存在多少个Weak<T>
引用,类似于strong_count
。其区别在于weak_count
无需计数为0
就能使Rc<T>
实例被清理;
- 调用
- 强引用代表如何共享
Rc<T>
值的所有权,但弱引用并不属于所有权关系。它们不会造成引用循环,因为任何弱引用的循环会在其相关的强引用计数为0
时被打断。 - 调用
Weak<T>
实例的upgrade
方法,这会返回Option<Rc<T>>
。如果Rc<T>
值还未被丢弃,则结果是Some
;如果Rc<T>
已被丢弃,则结果是None
。因为upgrade
返回一个Option<T>
,我们确信 Rust 会处理Some
和None
的情况,所以它不会返回非法指针。