智能指针
其实引用就是一种指针,但是在rust中,引用是只借用数据的指针,而大多数智能指针本身就拥有他们指向的数据。
智能指针实现了Deref与Drop两个trait,前者使得智能指针的实例拥有与引用一致的行为,后者可以自定义智能指针离开作用域时运行的代码。
标准库中常见的智能指针有:
Box<T>:用来在堆上分配值。Rc<T>:允许拥有多重所有权的引用计数类型。Ref<T>和RefMut<T>:可以通过RefCell<T>访问,是一种可以在运行时而不是编译时执行借用规则的类型。
Box装箱
这是最简单直接地一种智能指针。将数据存储在堆上,并在栈中保留一个指向堆数据的指针。 通常用来保存一个无法在编译器确定大小的数据类型。
使用Box存储数据
语法非常简单,示例如下:
fn main() {
let b = Box::new(5);
println!("b = {}", b);
}当Box离开作用域,栈上的指针和堆上的数据都会释放。
使用装箱定义递归类型
递归类型可以自身嵌套自己的类型,所以是无法在编译期确定大小的,但是Box是由一个固定大小的。只要在递归类型的定义中使用装箱就可以在编译期确定大小了。
例如一个链表:
enum List {
Cons(i32, List),
Nil,
}上面的代码无法通过编译,因为这个递归类型无法在编译期确定类型大小。 使用大小固定的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))))));
}Deref trait
实现Deref可以自定义解引用运算符*的行为。这就可以将智能指针当作常规引用一样使用,意味着原本处理引用的代码可以无需修改就可以用于处理智能指针。
使用解引用跳转到指针指向的值
因为指向就是存储了一个数据的地址,通过对其解引用获得数据。
fn main() {
let x = 5;
let y = &x;
assert_eq!(5, x);
assert_eq!(5, *y);
}把Box当作引用来操作
智能指针也能够通过解引用跳转到对应的数据,与上面保持一致。
fn main() {
let x = 5;
let y = Box::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}之所以智能指针可以解引用就在于实现了Deref trait。
对于一个自定义的智能指针
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
}
}*运算符会被替换成*(y.deref()),即一个deref函数和一个朴素的*运算符。
函数和方法的隐式解引用
当我们将某个特定类型的值引用作为参数传递给函数或方法,但传入的类型与参数类型不一致时,解引用转换就会自动发生。编译器会插入一系列的deref方法调用来将我们提供的类型转换为参数所需的类型。
Rust通过实现解引用转换的功能,使得调用函数或者方法的时候无需多次显式调用&和*运算符。
这一过程在编译期进行,不会有性能影响。
解引用与可变性
使用Deref trait能够重载不可变引用的*运算符。与之类似,使用DerefMut trait能够重载可变引用的*运算符。
- 当T: Deref<Target=U>时,允许&T转换为&U。
- 当T: DerefMut<Target=U>时,允许&mut T转换为&mut U。
- 当T: Deref<Target=U>时,允许&mut T转换为&U。
Drop trait
用来自定义在清理时运行的代码。 在Rust中,我们可以为值指定离开作用域时需要执行的代码,而编译器则会自动将这些代码插入到合适的地方。因此不需要开发者关注何时释放。
通过实现Drop trait来指定值离开作用域时需要运行的代码。Drop trait要求实现一个接收self可变引用作为参数的drop函数。
通过如下示例来观察调用Drop函数的时间:
struct CustomSmartPointer {
data: String,
}
impl Drop for CustomSmartPointer {
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.");
}变量的丢弃顺序与创建顺序相反,所以d要比c先丢弃。
使用std::mem::drop提前丢弃值
使用智能指针管理锁的时候,也许会遇到希望强制运行drop函数来释放锁,以便让同作用域中的其它代码来获取它的情景。
但是Rust不允许手动调用drop函数,如果想要清理,需要调用标准库的std::mem::drop函数来清理。这个函数放入了预导入模块,直接可以使用。
基于引用计数的智能指针Rc
要注意,Rc是支持多重所有权的。这是与至今为止的所有权相冲突的,但是实际使用当中确实是存在一个数据拥有多个所有者的,也正是因为才提供了Rc这种的后门,来专门处理这种情况。
Rc的类型实例会在内部维护一个用于引用计数的计数器。当一个值的引用计数器的值为0的时候,才会清理掉这个数据。
更要注意,Rc只能用于单线程。 示例:
use std::rc::Rc;
enum List {
Cons(i32, Rc<List>),
Nil,
}
use crate::List::{Cons, Nil};
fn main() {
let a = Rc::new(Cons(5,Rc::new(Cons(10, Rc::new(Nil)))));
let b = Rc::new(Cons(3, Rc::clone(&a)));
let c = Rc::new(Cons(4, Rc::clone(&a)));
}调用Rc::clone(&a)只会增加引用计数。
Rc通过不可变引用使你可以在不同部分间共享只读数据,多个指向同一区域可变借用会导致数据竞争及数据不一致。但是有的时候数据可变是非常有必要的,这就是接下来介绍的内部可变性及RefCell类型。
RefCell类型与内部可变性
内部可变性是Rust的设计模式之一,允许你在持有不可变引用的前提下修改数据。这在一般情形下是会被禁止的,内部可变性在它的数据结构内部使用了unsafe代码来绕过Rust正常的可变性和借用规则。
RefCell就是使用了内部可变性模式的类型。
RefCell在运行时检查借用规则
一般引用和Box代码,都会在编译阶段强制代码遵守借用规则。而使用RefCell的代码只会在运行的时候进行检查并在违反规则之后触发panic。
编译期简单可以让很多错误提前暴露并且不会带来运行时开销。这也是Rust将编译期检查作为默认行为的原因。Rust强制编译期遵守其设计哲学,对于必须打破的,那么其提供专门的工具来解决,以保证特殊的情形特殊处理,与常规情形明确分隔开来。
我们在运行时能够保证每次只有一个引用能够修改数据,但是对于编译器来说,就是存在指向同一数据的多个可变借用,为此提供了RefCell来绕过编译期检查。
Box,Rc,RefCell的选择依据:
- Rc允许一个数据有多个所有者。Box和RefCell都只有一个所有者。
- Box和Rc都是编译期检查借用,但是Rc只能是不可变借用。RefCell在运行时检查借用。
- RefCell本身不可变,但是其内部数据可变。
RefCell提供了borrow和borrow_mut方法分别返回不可变借用Ref<T>和可变借用RefMut<T>。这两者都是指针指针可以当作引用使用。RefCell只是将借用规则推迟到了运行时,所以仍需满足同时只能存在一个可变借用或者多个不可变借用。
borrow会将不可变计数加1,当Ref离开作用域释放的时候,不可变引用计数会减1。
Rc与RefCell实现一个拥有多重所有权的可变数据
#[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);
}循环引用
出现循环引用,导致每个指针的引用计数都不可能减少到0,对应的数据无法丢弃,造成内存泄漏。
为了解决这种情况,引入了Weak来代替Rc避免循环引用。通过Rc::downgrade来获得Rc的Weak智能指针,将Rc的weak_count计数加1,weak_count与strong_count不同的是,Rc释放的时候weak_count无需为0。
因为weak引用使用的使用需要确定数据是否还在,提供了upgrade方法来获得Rc。
这与c++中的shared_ptr和weak_ptr作用完全一致。
tags: 智能指针