0. Intro
Rust和C++一样,有智能指针的概念。
智能指针意为:除了有指向数据的地址外,还包含其它元数据。
因此在Rust中,很多都是智能指针,如
String、Vec<T>等。而在Rust中,引用就是普通指针,它只指向数据,并没有其它元数据。
此外,在Rust中,智能指针还有其它的特点:
- 通常拥有数据的所有权(而普通指针(引用)没有)
- 通常使用结构体实现,并实现
Deref和Droptrait (解引用,析构)
下面会记录Deref和Drop trait,并记录最常用的几个智能指针:
Box<T>:指向堆上的数据Rc<T>:引用计数智能指针Ref<T>,RefMut<T>,RefCell<T>:一个在运行时而不是在编译时执行借用规则的智能指针
此外还有内部可变性,引用循环的内容。
1. Box<T>:最普通的智能指针
Box<T>是一个普通的智能指针:
- 它将内部的数据保存到堆上,自己指向这份数据
- 它拥有这份数据的所有权
- 它大小确定,就是指针的大小
其中第四点是Box<T>的重要特点,它可以解决结构递归定义不被编译通过的问题,该问题的原因是:对于Rust结构体定义,必须编译期确定其大小,所以不能递归定义结构体。
例如下面的代码无法通过编译,原因是递归定义,无法确定结构体大小:
1 2 3 4 struct RecursiveList { id: i32, list: RecursiveList }但是使用
Box<T>后,即可通过编译,其大小可以确定为sizeof(i32) + sizeof(Box)(第2项就是Box指针,指针本身的大小是确定的):
1 2 3 4 struct RecursiveList { id: i32, list: Box<RecursiveList> }
而Box<T>作为智能指针,也实现了:
Deref:- 可将里面的
T直接作为引用使用 - 可以解引用(通过
*),但不能通过原有的Box<T>变量访问内部数据,因为被移动/丧失所有权了
- 可将里面的
DerefMut:在Deref基础上,提供“可修改”的功能Drop:当Box<T>离开作用域后,调用drop方法,会清理指针指向的数据,即数据T
2. Deref & Drop trait
智能指针必须实现2个trait:Deref和Drop。下面分别说明这2个trait。
2.1. Deref/DerefMut:将智能指针当作内部数据的引用使用
Deref trait有2个功能:
- 将智能指针当作内部数据的引用使用
- 实现解引用操作
DerefMut trait在Deref基础上,提供“可修改”的功能,但前提是实现Deref。
例如一个结构体
List:
1 2 3 4 struct List<T> { id: i32, list: Vec<T> }那么可以实现
Deref和DerefMuttrait:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 impl<T> Deref for List<T> { type Target = Vec<T>; // 注明解引用后返回的类型(实现需要确定trait的关联类型) /// 实现deref方法,返回引用 fn deref(&self) -> &Self::Target { &self.list } } impl<T> DerefMut for List<T> { /// 实现deref_mut方法,返回可变引用 fn deref_mut(&mut self) -> &mut Self::Target { &mut self.list } }那么它就实现了上述的2个功能:
1 2 3 4 5 let mut *l = List {id: 32, list: vec![123]}; l.len(); // 1: 直接当内部数据的引用,等效于list.deref().len(), 即&l.list.len() l.push(1); // 1: 直接当内部数据的引用,等效于list.deref_mut().push(1), 即&l.list.push(1) *l = vec![1234]; // 2: 解引用, 等效于*(list.deref_mut()), 即l.list, 可进行修改 let l2 = *list; // 2: 解引用, 等效于*(list.deref()), l.list所有权被转移到l2但要执行的方法中参数是
self(不是引用),那么智能指针deref/deref_mut的后的Target需要实现Copytrait,它会拷贝一个副本传入到方法中。这是因为,一个引用
&T执行一个参数是self的方法,Rust要求T实现Copytrait。
此外,函数和方法的引用参数,传参时,Rust编译器会根据Deref关系进行推导,我们不需要手动解引用:
T: Deref<Target=U>:参数是&self时,Rust根据Dereftrait进行推导&T$\rightarrow$&U&mut T$\rightarrow$&U
T: DerefMut<Target=U>:参数是&mut self时,Rust根据DerefMuttrait进行推导,若没实现,则编译错误&mut T$\rightarrow$&mut U
例如下面代码是合法的:
1 2 3 4 5 6 fn hello(arg: &str) { println!("hello {}", str); } let ptr = Box::new(String::from("123")); hello(&ptr);这边
&ptr的类型是&Box<String>:
- 它首先被转成
&String- 而
String也实现了Deref,会被转成&str,满足函数参数要求
2.2. Drop:析构
Drop trait需要实现drop方法,可将其看成一个析构函数:
- 它会在变量离开作用域后调用
- 然后清理变量和其指向的数据
Drop trait的drop不能显式调用,因为这会造成double free。
不过,若要提前释放内存,可调用std::mem::drop(_x: T)函数,它会执行Drop trait的drop函数,并释放内存。
std::mem:drop(_x: T)源码很简单,是一个函数。它将变量所有权给到函数内部,执行完函数后变量离开作用域,就执行drop并清理内存。因此,调用该函数后,外部再访问该变量,编译会不通过,因为所有权没了。
3. Rc<T>
3.1. Rc<T>:引用计数智能指针
Rc<T>类似于C++的std::shared_ptr,其内部维护引用计数:
Rc::new:创建一个Rc<T>,引用计数为1Rc::clone:返回一个新的Rc<T>,指向的数据和旧的Rc<T>一样,数据派生的所有Rc<T>引用计数+1- 离开作用域:数据派生的所有
Rc<T>引用计数-1,当为0时,清理内存(若T实现了Droptrait,也会调用对应的drop方法)
实际上
Rc<T>维护了2个引用计数:strong_count,weak_count。上述的引用计数指strong_count。至于
weak_count,后面会说。
例如下面代码:
1 2 3 4 5 6 let rc: Rc<&str> = Rc::new("content"); // strong_count = 1 let rc2: Rc<&str> = rc.clone(); // strong_count = 2 let rc3: Rc<&str> = rc.clone(); // strong_count = 3 // 这里, rc2这个指针本身被销毁, 后面不能再访问rc2(来访问内部数据) // 但是引用计数大于0, 内部数据没被清除, 还可以通过rc或rc3访问 std::mem::drop(rc2); // strong_count = 2
不过有以下几个注意点:
-
Rc<T>没有实现DerefMut,所以访问时,内部数据不可变;若要可变,需要往类型参数添加Cell或RefCell(后面会说)之所以不实现
DerefMut,是因为:若实现它,则可能出现多个可变借用,这个Rust借用规则冲突。 -
Rc<T>解引用不是移动,而是拷贝,因此要调用*rc作为右值,T需要实现Copytrait解释:考虑下面的代码,假如
Rc<T>解引用是移动,那么可能会出现内存访问错误,如野指针1 2 3 4 5
let rc = Rc::new(content); let rc2 = Rc::clone(&rc); // strong_count = 2 let deref: T = *rc2; // 解引用, 假如是移动, deref获得了content所有权 std::mem::drop(deref); // 这里content就被清除了 // ERROR: 这里rc指针本身可以访问, 但是content被清除, 成为野指针, 编译不通过
所以,为了内存访问安全,上面第3行的解引用必须是拷贝,所以内部数据类型
T必须实现Copytrait -
Rc<T>引用计数线程不安全,不能跨线程使用;若要跨线程使用引用计数,使用std::sync::Arc
3.2.RefCell<T>:运行时检查借用规则
在Rc<T>中,内部数据只能进行不可变借用。若要改变内部的数据,那么就要往内部数据里添加Cell或RefCell。这里着重讲RefCell。
通过Rc<RefCell<T>>改变内部的数据T,其原理就是在运行时检查借用规则,规则就是之前提过的:
- 所有权单一
- 至多一个可变引用,或多个不可变引用
- 引用有效
首先是RefCell,它涉及2个重要方法,以获取T的引用/可变引用,调用这2个方法需要满足借用规则,若运行时规则冲突,则会panic!:
borrow:返回一个Ref智能指针,其deref方法返回T的引用borrow_mut:返回一个RefMut智能指针,其deref方法返回T的可变引用
RefCell<T>常和Rc<T>一起使用,以可以让内部的数据可变:
1 2 3 4 5 6 7 8 let mut rc = Rc::new(RefCell::new(vec![123])); // Rc<RefCell<Vec<i32>>> let mut bm = rc.borrow_mut(); // RefMut<Vec<i32>>, // 这里rc是智能指针, deref后等价于引用&RefCell<Vec<i32>> // borrow_mut里的参数是&self, 所以对Rc执行的是deref bm.push(1); // bm也是智能指针, deref后等价于可变引用&mut Vec<i32> // 此时Vec内的数据为[123, 1] *bm = vec![123, 2]; // 同上, 此时Vec数据替换成新的[123, 2] let b = rc.borrow(); // Ref<Vec<i32>>, 但运行时会panic, 因为借用规则冲突另外,
Ref<T>和RefMut<T>使用*解引用,作为右值时,和Rc<T>一样,执行的是拷贝而非移动,因此需要T实现Copytrait。
依旧需要注意:
- 它不会对借用进行静态检查,若运行时违反借用规则,则会
panic! - 它线程不安全
另一个就是Cell,它代表了一个可变的数据区块。它也主要涉及2个方法:
get:返回内部数据的一个拷贝,因此T需要实现Copytraitget_mut:返回内部数据的一个可变引用,即&mut T
在
Rc中使用Cell需要注意:若一个变量类型为Rc<Cell<T>>,若要获取T的可变引用,不能直接对该变量调用get_mut,因为Rc没有实现DerefMut。即下面代码不能通过编译:
1 2 3 4 let mut rc = Rc::new(Cell::new(vec![123])); // Rc<Cell<Vec<i32>>> let mut bm = rc.get_mut(); // ERROR: Rc没实现DerefMut // 无法deref_mut成&mut Cell<Vec<i32>> // get_mut参数是&mut self, 所以对Rc执行的是deref_mut所以,在
Rc下,要改变内部的数据,只能通过Cell提供的方法来实现,包括:set,replace等:
1 2 3 let mut rc = Rc::new(Cell::new(vec![123])); // Rc<Cell<Vec<i32>>> rc.set(vec![123, 1]); // OK: 将内部Vec修改为[123, 1] // 因为set方法参数是&self, 所以对Rc执行的是deref
3.3. Weak<T>:解决引用计数下循环引用带来的内存泄漏
总所周知,引用计数的GC无法解决循环引用的垃圾回收问题,造成内存泄漏。
Rust无法捕捉所有的循环引用,所以为了避免循环引用:
- 重新涉及数据结构,使得所有权单一
Rc<T>可以降级为Weak<T>,以解决循环引用问题
例如:一颗树,节点需要引用父节点,也要引用子节点,那么子节点可以作为
Rc<T>,父节点可以作为Weak<T>,以避免循环引用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 struct Node { value: i32, parent: RefCell<Weak<Node>>, children: RefCell<Vec<Rc<Node>>>, } let mut child = Rc::new( Node { value: 3, parent: RefCell::new(Weak::new()), children: RefCell::new(vec![]) } ); let mut parent = Rc::new( Node { value: 5, parent: RefCell::new(Weak::new()), children: RefCell::new(vec![child.clone()]) } ); // 左值类型: *&mut Weak<Node>, 可直接赋值 *child.parent.borrow_mut() = Rc::downgrade(&parent); println!("strong:{},{}", Rc::strong_count(&parent), Rc::strong_count(&child)); // 1 2 println!("weak:{},{}", Rc::weak_count(&parent), Rc::weak_count(&child)) // 1 0
Weak<T>和Rc<T>有一定的关系:
Rc<T>可通过downgrade,得到Weak<T>,此时Rc<T>的weak_count+1Weak<T>可通过upgrade,得到Option<Rc<T>>- 当
strong_count > 0,则能升级到Rc<T>,即返回的是Some,此时strong_count+1 - 当
strong_count == 0,则原引用失效,不能升级,即返回的是None
- 当
- 离开作用域时:
Weak<T>:weak_count-1Rc<T>:strong_count-1,当其为0时,T被清理,不需要weak_count == 0