这一篇介绍Rust中常用的智能指针以及如何自定义智能指针~
&
获取指针(当然根据前面的文章中可知这种说法其实不太准确,但使用引用的场景基本都离不开&
运算符);String
和Vec
,它们都拥有一下智能指针的特点:
Box
:用于在堆上分配值;Rc
:一个引用计数类型,其数据可以有多个所有者;Ref
和 RefMut
:通过 RefCell
访问。( RefCell
是一个在运行时而不是在编译时执行借用规则的类型);这里将介绍几种最基础的智能指针:
智能指针类型 | 简介 | 允许所有者数目 | 何时检查借用规则 | 谁保证借用规则通过 | 被引值是否可变 | 使用场景 |
---|---|---|---|---|---|---|
Box |
最普通的智能指针 | 一个(独占所有权) | 编译期 | 编译器 | 不可变、可变 | 没要求 |
Rc |
引用计数智能指针 | 多个(共享所有权) | 编译期 | 编译器 | 不可变 | 单线程 |
RefCell |
内部可变性(外部不可变,内部可变) | 一个(独占所有权) | 运行时 | 开发者 | 不可变、可变 | 单线程 |
Box
:最普通的智能指针Box
是指向类型为T
的堆内存分配值的智能指针,可以通过解引用操作符来获取Box
中的T
。当Box
超过作用域范围时,Rust会自动调用其析构函数,销毁内部对象,并释放所占堆内存。Box
会在堆上存储数据,创建语法为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
类型是智能指针,因为它实现了:
Deref
trait ,使Box
值允许被当做引用对待;Drop
trait ,使Box
值离开作用域时,box
(Box
里指向实际数据的字段)所指项的数据也会被清除;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,
}
Rc
:引用计数智能指针Rc
智能指针来引用计数。Rc
允许一个值有多个所有者,引用计数确保了只要还存在所有者,该值既保持有效。每当值共享所有权时,计数会增加一。当计数为零,即所有共享变量离开作用域时,该变量才会被析构。Rc
用于希望堆上分配的数据可以供程序的多个部分读取,并且无法在编译时确定哪个部分是最后使用者的场景。Rc
是单线程引用计数指针,不是线程安全的类型,不允许将引用计数传递或共享给其它线程。Rc
定义于std::rc
模块,所以使用前需要先use std::rc;
Rc
的时候使用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
实例会自动计数减一,当计数为0,说明没有所有者,则清除被引用值。Rc
允许在程序的多个部分之间只读地共享数据。如果Rc
也允许多个可变引用,则会违反借用规则之一:相同位置的多个可变借用可能造成数据竞争和不一致。RefCell
:处理内部可变性场景unsafe
代码来模糊 Rust 通常的可变性和借用规则,所涉及的 unsafe
代码将被封装进安全的 API 中,而外部类型仍然是不可变的;RefCell
并没有完全绕开借用规则,编译器的借用检查器允许内部可变并在运行时执行借用检查。如果在运行时出现了违反借用的规则,比如有多个可变借用会导致程序错误。RefCell
通过将借用规则的检查时期延后至运行时,使修改一个不可变值的内存数据(字段)称为可能,只要修改时仍满足借用规则即可。RefCell
通过引入少量的性能损耗,但是会使得代码更灵活。RefCell
只适用于单线程场景。RefCell
提供的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
会记录当前有效的Ref
和RefMut
智能指针的数量。在任何时候,同一作用域中只允许有多个Ref
或一个RefMut
。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
常配合Rc
来使用。Rc
允许数据有多个所有者,但只能提供数据的不可变访问。如果两者结合使用,Rc>
表面上是不可变的,但利用RefCell
的内部可变性可以在需要时修改数据。Rc
结合 RefCell
实现值拥有多个可变引用Rc
和RefCell
结合使用,具体做法为:Rc>
,则可以使同一份数据可以有多个RefCell
引用,每一个RefCell
引用又可以对改数据进行可变操作。Cell
:通过复制来访问数据。Mutex
:用于实现跨线程情形下的内部可变性模式。Deref
和Drop
两个trait,只要实现了这两个trait,就会被认为是智能指针,这两个trait的作用如下:
Deref
trait:允许自定义智能指针的值表现得像引用一样,统一对智能指针和引用的操作。比如说指针的值为p
,则当使用*p
时将会执行Deref
中定义的方法(如果需要返回可变引用,需要实现DerefMut
trait);Drop
trait:允许开发者自定义当智能指针离开作用域时运行的代码;Deref
trait:解引用Deref
trait的类型的值val
支持使用解引用符*
运算,即*val
(提供解引用功能)。Deref
trait,且方法会返回智能指针包装的引用(只读引用)。Box
是如何实现Deref
trait的Box
是一个实现了Deref
trait的tuple struct,并重载了deref(&self) -> &Self::Target
方法。Box
实现了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())
}
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
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
traitDerefMut
trait重载可变引用的*
运算符。Deref
trait重载不可变引用的*
运算符。DerefMut
trait重载可变引用的*
运算符。T: Deref
时从 &T
到 &U
。T: DerefMut
时从 &mut T
到 &mut U
。T: Deref
时从 &mut T
到 &U
。Drop
trait:清理资源Drop
trait的类型的值在离开作用域时,会自动执行drop
方法。Drop
trait,会在drop
方法中执行如释放内存,文件资源或网络资源关闭等操作。drop
方法只会在值不再使用时被调用一次。drop
方法时,会按照变量声明顺序的反顺序执行(因为创建函数参数时需要入栈)。Drop
trait 要求实现一个叫做 drop
的方法,它获取一个 self
的可变引用。这个drop
函数也叫析构参数(destructor)fn drop(&mut self) { }
Drop
位于preclude模块中。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!
}
Rc
可能会发生引用循环,造成逻辑上的内存泄漏。Rc
改为Weak
。Rc::downgrade
并传递 Rc
值的引用来创建其值的 弱引用(weak reference):
Rc::downgrade
时会得到 Weak
类型的智能指针;Rc
值的 strong_count
加1
,调用 Rc::downgrade
会将 weak_count
加1
;Rc
类型使用 weak_count
来记录其存在多少个 Weak
引用,类似于 strong_count
。其区别在于 weak_count
无需计数为 0
就能使 Rc
实例被清理;Rc
值的所有权,但弱引用并不属于所有权关系。它们不会造成引用循环,因为任何弱引用的循环会在其相关的强引用计数为 0
时被打断。Weak
实例的 upgrade
方法,这会返回 Option>
。如果 Rc
值还未被丢弃,则结果是 Some
;如果 Rc
已被丢弃,则结果是 None
。因为 upgrade
返回一个 Option
,我们确信 Rust 会处理 Some
和 None
的情况,所以它不会返回非法指针。