Bootstrap

Rust学习(七):智能指针

Rust学习(七):智能指针

作为一种编程语言,Rust中最常见的指针类型就是引用,而引用只是一种普通指针,除引用数据之外,没有其他功能。为了更方便的处理复杂的结构数据,有必要在开始我们的算法之旅前,介绍Rust中智能指针的概率,当然,这里我们只会简单的介绍一些常用的智能指针,如果大家对此感到困惑,或者是想进一步深入的学习智能指针的设计哲学,建议阅读其Rust学习资料(doge)。

智能指针是一种数据结构,其行为类似于指针,含有元数据,在大部分情况下拥有数据的所有权,提供内存管理或绑定等附加功能,如管理文件句柄和网络链接。最早出现在C++语言中,后被Rust借鉴。Rust为只能指针封装了两大trait——Deref和Drop,当变量实现了Deref和Drop之后,就具备成为智能指针的基础了,Deref,为变量重载了解引用运算符“*”,类似与C++中的构造函数(类比方便理解),实现Drop之后,变量在超出作用域之后,就会自动从堆中释放,这就类似于C++中的析构函数了,总体而言,Rust中的智能指针具备如下特征:

  • 大部分情况下具有指向数据的所有权
  • 一般使用结构体实现
  • 实现了Deref和Drop两大trait

下面我们会介绍Deref和Drop两大trait的使用案例,以及一个典型的智能指针Box,为大家抛砖引玉。

1、Deref和Drop实现:

智能指针中Deref和Drop是最重要的两个trait。下面我们通过自定义的数据类型SBox实现Deref和Drop,来体会引用和智能指针的区别。首先我们实现一个没有Deref的SBox:

struct SBox<T>(T);  //定义一个元组结构体
impl<T> SBox<T> {
    fn new(x:T) -> Self {
        Self(x)
    }
}

fn main() {
    let x = 10;
    let y = SBox::new(x);
    println!("x:{x}");
    // println!("y:{}", *y) 错误!*没有被重载,不能使用*y进行解引用操作
}

下面为SBox实现Deref和Drop:

use::std::ops::{Deref, Drop};
impl<T> Deref for SBox<T> {
    type Target = T //设置解引用之后的返回值类型
    fn deref(&self) ->&Self::Target {
        &self.0  //访问元组结构体SBox<T>(T)中的T
    }
}

impl<T> Drop for SBox<T> {
    fn drop(&mut self) {
        println!("SBpx drop itself!");
    }
}

fn main() {
    let = x;
    let y = SBox::new(x);
    println!("x:{x}");
    println!("y:{}", *y);
}

数据存放在堆上,实现Deref之后,可以自动解引用:

fn main() {
    let num = 10;
    let n_box = Box::new(num);
    println!("n_box = {}", n_box);  //自动解引用,相当于*(y.deref())
}

2、Box:

最简单直接的智能指针是 box,其类型是 Box<T>。box 允许你将一个值放在堆上而不是栈上。留在栈上的则是指向堆数据的指针。

Box<T> 类型是一个智能指针,因为它实现了 Deref trait,它允许 Box<T> 值被当作引用对待。当 Box<T> 值离开作用域时,由于 Box<T> 类型 Drop trait 的实现,box 所指向的堆数据也会被清除。

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

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

(1)Box 在堆上存储数据:

在讨论 Box<T> 的堆存储用例之前,让我们熟悉一下语法以及如何与储存在 Box<T> 中的值进行交互。

fn main() {
    let b = Box::new(5);
    println!("b = {b}");
}

这里定义了变量 b,其值是一个指向被分配在堆上的值 5Box。这个程序会打印出 b = 5;在这个例子中,我们可以像数据是储存在栈上的那样访问 box 中的数据。正如任何拥有数据所有权的值那样,当像 b 这样的 box 在 main 的末尾离开作用域时,它将被释放。这个释放过程作用于 box 本身(位于栈上)和它所指向的数据(位于堆上)。

将一个单独的值存放在堆上并不是很有意义,所以像示例这样单独使用 box 并不常见。将像单个 i32 这样的值储存在栈上,也就是其默认存放的地方在大部分使用场景中更为合适。让我们看看一个不使用 box 时无法定义的类型的例子。

(2)Box创建递归类型:

递归类型recursive type)的值可以拥有另一个同类型的值作为其自身的一部分。但是这会产生一个问题,因为 Rust 需要在编译时知道类型占用多少空间。递归类型的值嵌套理论上可以无限地进行下去,所以 Rust 不知道递归类型需要多少空间。因为 box 有一个已知的大小,所以通过在循环类型定义中插入 box,就可以创建递归类型了。

作为一个递归类型的例子,让我们探索一下 cons list。这是一个函数式编程语言中常见的数据类型,来展示这个(递归类型)概念。除了递归之外,我们将要定义的 cons list 类型是很直白的,所以这个例子中的概念,在任何遇到更为复杂的涉及到递归类型的场景时都很实用。

cons list 是一个来源于 Lisp 编程语言及其方言的数据结构,它由嵌套的列表组成。它的名字来源于 Lisp 中的 cons 函数(“construct function" 的缩写),它利用两个参数来构造一个新的列表。通过对一个包含值的列表和另一个值调用 cons,可以构建由递归列表组成的 cons list。

例如这里有一个包含列表 1,2,3 的 cons list 的伪代码表示,其每一个列表在一个括号中:

(1, (2, (3, Nil)))

cons list 的每一项都包含两个元素:当前项的值和下一项。其最后一项值包含一个叫做 Nil 的值且没有下一项。cons list 通过递归调用 cons 函数产生。代表递归的终止条件(base case)的规范名称是 Nil,它宣布列表的终止。注意这不同于第六章中的 “null” 或 “nil” 的概念,它们代表无效或缺失的值。

cons list 并不是一个 Rust 中常见的类型。大部分在 Rust 中需要列表的时候,Vec<T> 是一个更好的选择。其他更为复杂的递归数据类型 确实 在 Rust 的很多场景中很有用,不过通过以 cons list 作为开始,我们可以探索如何使用 box 毫不费力的定义一个递归数据类型。

下面定义一个包含 cons list 的枚举。注意这还不能编译因为这个类型没有已知的大小,之后我们会展示:

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

使用这个 cons list 来储存列表 1, 2, 3 将看起来如下所示:

use crate::List::{Cons, Nil};

fn main() {
    let list = Cons(1, Cons(2, Cons(3, Nil)));
}

第一个 Cons 储存了 1 和另一个 List 值。这个 List 是另一个包含 2Cons 值和下一个 List 值。接着又有另一个存放了 3Cons 值和最后一个值为 NilList,非递归成员代表了列表的结尾。

如果尝试编译,就会遇到下面的错误:

$ cargo run
   Compiling cons-list v0.1.0 (file:///projects/cons-list)
error[E0072]: recursive type `List` has infinite size
 --> src/main.rs:1:1
  |
1 | enum List {
  | ^^^^^^^^^
2 |     Cons(i32, List),
  |               ---- recursive without indirection
  |
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
  |
2 |     Cons(i32, Box<List>),
  |               ++++    +

error[E0391]: cycle detected when computing when `List` needs drop
 --> src/main.rs:1:1
  |
1 | enum List {
  | ^^^^^^^^^
  |
  = note: ...which immediately requires computing when `List` needs drop again
  = note: cycle used when computing whether `List` needs drop
  = note: see https://rustc-dev-guide.rust-lang.org/overview.html#queries and https://rustc-dev-guide.rust-lang.org/query.html for more information

Some errors have detailed explanations: E0072, E0391.
For more information about an error, try `rustc --explain E0072`.
error: could not compile `cons-list` (bin "cons-list") due to 2 previous errors

因为 Rust 无法计算出要为定义为递归的类型分配多少空间,所以编译器给出了一个包括了有用建议的错误:

help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to make `List` representable
  |
2 |     Cons(i32, Box<List>),
  |               ++++    +

在建议中,“indirection” 意味着不同于直接储存一个值,应该间接的储存一个指向值的指针。

因为 Box<T> 是一个指针,我们总是知道它需要多少空间:指针的大小并不会根据其指向的数据量而改变。这意味着可以将 Box 放入 Cons 成员中而不是直接存放另一个 List 值。Box 会指向另一个位于堆上的 List 值,而不是存放在 Cons 成员中。从概念上讲,我们仍然有一个通过在其中 “存放” 其他列表创建的列表,不过现在实现这个概念的方式更像是一个项挨着另一项,而不是一项包含另一项。

因此,我们可以修改示例中 List 枚举的定义和对 List 的应用,这是可以编译的:

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

use crate::List::{Cons, Nil};

fn main() {
    let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}

Cons 成员将会需要一个 i32 的大小加上储存 box 指针数据的空间。Nil 成员不储存值,所以它比 Cons 成员需要更少的空间。现在我们知道了任何 List 值最多需要一个 i32 加上 box 指针数据的大小。通过使用 box,打破了这无限递归的连锁,这样编译器就能够计算出储存 List 值需要的大小了。

智能指针的内容纷繁复杂,但是仔细研究其背后的编程哲学还是具有相当的价值。

;