0. Intro
Rust和C++一样,有智能指针的概念。
智能指针意为:除了有指向数据的地址外,还包含其它元数据。
因此在Rust中,很多都是智能指针,如
String
、Vec<T>
等。而在Rust中,引用就是普通指针,它只指向数据,并没有其它元数据。
此外,在Rust中,智能指针还有其它的特点:
- 通常拥有数据的所有权(而普通指针(引用)没有)
- 通常使用结构体实现,并实现
Deref
和Drop
trait (解引用,析构)
下面会记录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
和DerefMut
trait:
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
需要实现Copy
trait,它会拷贝一个副本传入到方法中。这是因为,一个引用
&T
执行一个参数是self
的方法,Rust要求T
实现Copy
trait。
此外,函数和方法的引用参数,传参时,Rust编译器会根据Deref
关系进行推导,我们不需要手动解引用:
T: Deref<Target=U>
:参数是&self
时,Rust根据Deref
trait进行推导&T
$\rightarrow$&U
&mut T
$\rightarrow$&U
T: DerefMut<Target=U>
:参数是&mut self
时,Rust根据DerefMut
trait进行推导,若没实现,则编译错误&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
实现了Drop
trait,也会调用对应的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
需要实现Copy
trait解释:考虑下面的代码,假如
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
必须实现Copy
trait -
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
实现Copy
trait。
依旧需要注意:
- 它不会对借用进行静态检查,若运行时违反借用规则,则会
panic!
- 它线程不安全
另一个就是Cell
,它代表了一个可变的数据区块。它也主要涉及2个方法:
get
:返回内部数据的一个拷贝,因此T
需要实现Copy
traitget_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