智能指针

178 阅读5分钟

Box<T>

使用场景

用于在堆上分配值,数据被储存在堆上而不是栈上,没有性能损失。不过也没有很多额外的功能。它们多用于如下场景:

  • 当有一个在编译时未知大小的类型,而又想要在需要确切大小的上下文中使用这个类型值的时候
  • 当有大量数据并希望在确保数据不被拷贝的情况下转移所有权的时候
  • 当希望拥有一个值并只关心它的类型是否实现了特定 trait 而不是其具体类型的时候

场景一:当有一个在编译时未知大小的类型,而又想要在需要确切大小的上下文中使用这个类型值的时候

Rust 需要在编译时知道类型占用多少空间,如果一种类型在编译时无法知道具体的大小,那么被称为动态大小类型 (DST)。如递归类型

enum List { 
    Cons(i32, List), 
    Nil, 
}

以上就是函数式语言中常见的 Cons List,它的每个节点包含一个 i32 值,还包含了一个新的 List,因此这种嵌套可以无限进行下去,Rust 认为该类型是一个 DST 类型,并给予报错:

error[E0072]: recursive type `List` has infinite size //递归类型 `List` 拥有无限长的大小
 --> src/main.rs:3:1
  |
3 | enum List {
  | ^^^^^^^^^ recursive type has infinite size
4 |     Cons(i32, List),
  |               ---- recursive without indirection

此时若想解决这个问题,就可以使用我们的 Box<T>


enum List {
    Cons(i32, Box<List>),
    Nil,
}

只需要将 List 存储到堆上,然后使用一个智能指针指向它,即可完成从 DST 到 Sized 类型(固定大小类型)的华丽转变。

场景二:当有大量数据并希望在确保数据不被拷贝的情况下转移所有权的时候

fn main() {
    // 在栈上创建一个长度为1000的数组
    let arr = [0;1000];
    // 将arr所有权转移arr1,由于 `arr` 分配在栈上,因此这里实际上是直接重新深拷贝了一份数据
    let arr1 = arr;

    // arr 和 arr1 都拥有各自的栈上数组,因此不会报错
    println!("{:?}", arr.len());
    println!("{:?}", arr1.len());

    // 在堆上创建一个长度为1000的数组,然后使用一个智能指针指向它
    let arr = Box::new([0;1000]);
    // 将堆上数组的所有权转移给 arr1,由于数据在堆上,因此仅仅拷贝了智能指针的结构体,底层数据并没有被拷贝
    // 所有权顺利转移给 arr1,arr 不再拥有所有权
    let arr1 = arr;
    println!("{:?}", arr1.len());
    // 由于 arr 不再拥有底层数组的所有权,因此下面代码将报错
    // println!("{:?}", arr.len());
}

当栈上数据转移所有权时,实际上是把数据拷贝了一份,最终新旧变量各自拥有不同的数据,因此所有权并未转移。

而堆上则不然,底层数据并不会被拷贝,转移所有权仅仅是复制一份栈中的指针,再将新的指针赋予新的变量,然后让拥有旧指针的变量失效,最终完成了所有权的转移

场景三:当希望拥有一个值并只关心它的类型是否实现了特定 trait 而不是其具体类型的时候

如果我们想实现一个不同类型数据组成的数组,特征是一种很方便的形式。 如 我们想要一个实现了Draw特征的数据组成的数组。

由于我们并不知道数组中实现了Draw类型的数据的大小,所以使用Box<dyn Draw>将实现了Draw类型的数据存放到堆上

trait Draw {
    fn draw(&self);
}

struct Button {
    id: u32,
}
impl Draw for Button {
    fn draw(&self) {
        println!("这是屏幕上第{}号按钮", self.id)
    }
}

struct Select {
    id: u32,
}

impl Draw for Select {
    fn draw(&self) {
        println!("这个选择框贼难用{}", self.id)
    }
}

fn main() {
    let elems: Vec<Box<dyn Draw>> = vec![Box::new(Button { id: 1 }), Box::new(Select { id: 2 })];

    for e in elems {
        e.draw()
    }
}

Box解引用

一个由Box对象组成的数组Vec<Box<i32>>其存储模式如下图:


                    (heap)
(stack)    (heap)   ┌───┐
┌──────┐   ┌───┐ ┌─→│ 1 │
│ vec  │──→│B1 │─┘  └───┘
└──────┘   ├───┤    ┌───┐
           │B2 │───→│ 2 │
           ├───┤    └───┘
           │B3 │─┐  ┌───┐
           ├───┤ └─→│ 3 │
           │B4 │─┐  └───┘
           └───┘ │  ┌───┐
                 └─→│ 4 │
                    └───┘

上面的 B1 代表被 Box 分配到堆上的值 1

可以看出智能指针 vec 依然是存储在栈上,然后指针指向一个堆上的数组,该数组中每个元素都是一个 Box 智能指针,最终 Box 智能指针又指向了存储在堆上的实际值。

因此当我们从数组中取出某个元素时,取到的是对应的智能指针 Box,需要对该智能指针进行解引用,才能取出最终的值:

fn main() {
    let arr = vec![Box::new(1), Box::new(2)];
    let (first, second) = (&arr[0], &arr[1]);
    let sum = **first + **second;
}

以上代码有几个值得注意的点:

  • 使用 & 借用数组中的元素,否则会报所有权错误
  • 表达式不能隐式的解引用,因此必须使用 ** 做两次解引用,第一次将 &Box<i32> 类型转成 Box<i32>,第二次将 Box<i32> 转成 i32

Deref解引用

常规引用解引用

常规引用是一个指针类型,包含了目标数据存储的内存地址。对常规引用使用 * 操作符,就可以通过解引用的方式获取到内存地址对应的数据值

fn main() {
    let x = 5; // x在栈中保存了一个值
    let y = &x; // y引用x的值,但实际上存储的是这个值得内存地址

    assert_eq!(5, y);  // 报错can't compare `{integer}` with `&{integer}`
    // 虽然两者值相等,但y本身是一个常规引用类型,引用类型不能和值直接比较
    assert_eq!(5, *y); // 解引用后可以获取引用的内存地址中的值,因此可以与5比较
}

智能指针解引用

智能指针是一个结构体类型,如果使用*解引用,需要一个复杂的流程,否则编译器无法直接对其进行*myStruct