Rust笔记:智能指针

Posted by keys961 on February 10, 2020

0. Intro

Rust和C++一样,有智能指针的概念。

智能指针意为:除了有指向数据的地址外,还包含其它元数据。

因此在Rust中,很多都是智能指针,如StringVec<T>等。

而在Rust中,引用就是普通指针,它只指向数据,并没有其它元数据。

此外,在Rust中,智能指针还有其它的特点:

  • 通常拥有数据的所有权(而普通指针(引用)没有)
  • 通常使用结构体实现,并实现DerefDrop trait (解引用,析构)

下面会记录DerefDrop 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:DerefDrop。下面分别说明这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>
}

那么可以实现DerefDerefMut 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>,引用计数为1
  • Rc::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,所以访问时,内部数据不可变;若要可变,需要往类型参数添加CellRefCell(后面会说)

    之所以不实现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>中,内部数据只能进行不可变借用。若要改变内部的数据,那么就要往内部数据里添加CellRefCell。这里着重讲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 trait
  • get_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 +1
  • Weak<T>可通过upgrade,得到Option<Rc<T>>
    • strong_count > 0,则能升级到Rc<T>,即返回的是Some,此时strong_count +1
    • strong_count == 0,则原引用失效,不能升级,即返回的是None
  • 离开作用域时:
    • Weak<T>weak_count -1
    • Rc<T>strong_count -1,当其为0时,T被清理,不需要weak_count == 0