智能指针

其实引用就是一种指针,但是在rust中,引用是只借用数据的指针,而大多数智能指针本身就拥有他们指向的数据。

智能指针实现了DerefDrop两个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提供了borrowborrow_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: 智能指针