Rust 智能指针 Box, Rc, ReCell 的使用 --学习向

Rust 智能指针 Box, Rc, ReCell 的使用 --学习向

在其他可以使用引用和指针的语言中(如 C,Java 等),我们需要使用链表之类递归类型的时候,通常只需要在该类型中包含自身的引用即可:

public class ListNode {
    private int val;
    public ListNode next;
    
    public ListNode(int v) {
        val = v;
    }
}

由于 Java 对象都是 new 出来的,如果没有通过逃逸分析的对象都是在堆上分配内存,计算过程移动到运行时,同时返回一个指向这一块内存的指针,即引用。

而在 Rust 中,默认是在栈上分配(生命周期标记为 ‘static 的对象除外),这就要求 Rust 编译器需要在编译期直接计算出内存占用大小。而当需要使用链表这种递归类型的时候,在不使用智能指针的情况下,Rust 编译器依照枚举的内存计算方法无法计算出这种链表的内存占用大小,以至于在编译时拒绝编译代码并抛出 panic: recursive type has infinite size

所以如果要实现这种动态引用,就需要使用 Box, Rc, ReCell 这种智能指针来协助 Rust 获知对象占用的大小,以便获知其分配的内存大小。

Box<T>

首先我们需要声明一个节点类型,用于表示我们声明出的 Rust 链表元素:

#[derive(debug)]
pub enum List {
    Node(i32, Box<List>), 
    Nil,
}

当需要使用时:

fn main() {
    let list_node = ListNode(0, 
        Box::new(Node(1, 
            Box::new(Node(2, 
                Box::new(Nil))))));
    println!("List => {}", list_node);
}

其原理就在于,Box<T> 作为一个智能指针我们是可以计算出其内存占用大小的,当 new 的动作发生时,就在栈上分配一块 Box<T> 大小的内存,并将其指向堆中的元素,这样我们就可以知道在栈上,一个节点的大小就等于 i32 + Box

A finite Cons list

Rc<T>

上面这种单链表使用 Box<T> 就可以处理了,但是有时候我们又可能会出现多个节点同时指向一个节点的情况,即一个节点的所有权被多个节点获取。

Two lists that share ownership of a third list

那么这种情况下就需要使用 Rc<T> 引用计数智能指针来使得上面这种情况变为可能:

// 首先需要更改定义
#[derive(debug)]
pub enum List {
    Node(i32, Rc<List>), 
    Nil
}

fn main() {
    let a = Rc::new(5, Rc::new(Node(10, Rc::new(Nil))));
    let b = Node(3, Rc::clone(&a));
    let c = Node(4, Rc::clone(&a));
}

Rc::clone() 方法并不等同于 x.clone(),前者属于引用计数(Reference Counter),而后者属于数据的深拷贝。

值得注意的是, Rc<T> 引用是不可变的,如果是可变的,那么就会和借用规则出现冲突。同时 Rc<T> 只能在单线程情况下使用。

如果我们真的需要可变引用,那么可以考虑使用 RefCell<T>

RefCell<T>

假设我们需要在多所有权的基础上使得节点 Node 中所包含的值在运行时是可变的,那么可以这么写:

#[deirve(debug)]
pub enum List {
    List(Rc<RefCell<i32>>, Rc<List>), 
    Nil,
}

fn main() {
    let val_1 = Rc::new(RefCell::new(5));
    let val_2 = Rc::new(RefCell::new(10));
    let a = Rc::new(
        Node(Rc::clone(&val_1), Rc::new(
            Node(Rc::clone(&val_2), Rc::new(
                Nil)))));
    let b = Node(3, Rc::clone(&a));
    let c = Node(4, Rc::clone(&a));
    println!("Pre: {:?}", b);
    
    // update the value
    *val_1.borrow_mut() += 10;
    
    println!("Now: {:?}", b);
}

Output:

Pre: Cons(RefCell { value: 3 }, Cons(RefCell { value: 5 }, Cons(RefCell { value: 10 }, Nil)))
Now: Cons(RefCell { value: 3 }, Cons(RefCell { value: 15 }, Cons(RefCell { value: 10 }, Nil)))

值得注意的是,使用 RefCell<T> 智能指针的对象可以获得其可变引用(*ptr.borrow_mut())和不可变引用(*ptr.borrow())。

同时,和 Rc<T> 一样,RefCell<T> 也是只能在单线程情况下使用。

参考(图片来源)