Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

智能指针

指针是一个包含内存地址的变量;这个地址指向,或“指向“,一些其他数据。在 X 语言中,最常见的指针类型是我们在第 4 章中看到的引用。引用由 & 字符表示,并借用它们指向的值。除了引用数据之外,它们没有任何特殊功能。它们也没有任何开销,因此是最常用的指针类型。

相比之下,智能指针是数据结构,它们像指针一样行动,但也有额外的元数据和功能。智能指针的概念不是 X 语言独有的:它起源于 C++,并存在于其他语言中。X 语言在标准库中定义了各种智能指针,它们提供的功能超出了引用所提供的功能。为了探索一般概念,我们将看看一些不同的智能指针示例,最值得注意的是引用计数智能指针类型。这种指针允许你拥有多个数据所有者,通过跟踪所有者的数量,当没有所有者剩余时清理数据。

在 X 语言中,使用 struct 自定义类型是常见的,并且有一个关键的区别使智能指针与普通结构体不同:智能指针实现了 DerefDrop trait。Deref trait 允许智能指针实例的行为类似于引用,因此你可以编写适用于引用或智能指针的代码。Drop trait 允许你自定义当智能指针实例超出作用域时运行的代码。在本章中,我们将讨论这两个 trait,并演示为什么它们对智能指针很重要。

鉴于智能指针模式是 X 语言中经常使用的设计模式,本章不会涵盖标准库中可用的每一个智能指针。许多库都有自己的智能指针,你甚至可以自己编写一些。我们将介绍标准库中最常见的智能指针:

  • Box<T>,用于在堆上分配值
  • Rc<T>,一个引用计数类型,允许多重所有权
  • Ref<T>RefMut<T>,通过 RefCell<T> 访问,它在运行时而不是编译时强制借用规则

此外,我们将介绍内部可变性模式,其中不可变类型公开了一个用于改变内部值的 API。我们还将讨论引用循环:它们如何泄漏内存以及如何防止它们。

让我们开始吧!

使用 Box 指向堆上的数据

最直接的智能指针是 box,其类型写为 Box<T>。Box 允许你将数据存储在堆上,而不是栈上。栈上剩下的是指向堆上数据的指针。

让我们看看 Box<T> 是什么样的:

let b = Box::new(5)
println("b = {}", b)

我们使用 Box::new 定义了变量 b,它包含值 5。这段代码会打印 b = 5;在这种情况下,我们可以像访问栈上的数据一样访问 box 中的数据。与任何拥有的值一样,当 box 超出作用域时(就像在 main 结束时 b 一样),它将被释放。释放发生在 box(存储在栈上)和它指向的数据(存储在堆上)上。

将单个值放在堆上不是很有用,所以你不会经常像刚才那样单独使用 box。box 有用的情况是,当你有一个类型,其大小在编译时无法知道,并且你想在需要精确大小的上下文中使用该类型值时。让我们探索这种情况。

使用 Box 允许递归类型

box 的主要用例是当你有一个可能是递归的类型时——一个可以将自身的另一个实例作为自己的一部分的类型。因为 box 有一个已知的大小,我们可以通过在递归类型定义中插入 box 来创建递归类型。

让我们探索 cons list,这是一种主要在函数式编程语言中发现的数据结构,作为递归类型的示例。除了递归情况外,我们将定义的 cons list 是一个简单的结构;我们将用它来练习使用 box。

更多关于 cons list 的信息

cons list(或“construct list“)是一个源自 Lisp 编程语言及其方言的数据结构。一个 cons list 是一个嵌套对列表;它的构造函数的传统名称(因此得名 cons list)是 conscons 函数接受两个参数:一个值和另一个 cons 函数,并创建一个新的 cons 函数,该函数又包含该值和另一个 cons 函数,以此类推,直到我们到达 nilNil,这是结束条件,它不包含下一个值。cons 大致翻译为“构造函数“。

这是一个包含 1、2 和 3 的 cons list 的伪代码表示,每个 cons 函数都在括号中:

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

cons list 中的每个项目包含两个元素:当前项目的值和下一个项目。最后一个项目只包含一个名为 Nil 的值,没有下一个项目。我们通过递归调用 cons 函数来定义 cons list。我们使用特殊的 Nil 名称来指定 cons list 的结束条件。注意,这与我们在第 6 章中讨论的列表类型不同!cons list 不是现代 X 语言代码中常用的数据结构,但它是一个概念上清晰的例子,展示了 box 如何让我们定义递归类型。

让我们在示例中更详细地探索 cons list。

定义 cons list

首先,让我们定义一个包含第一个项目和下一个项目的枚举,我们称之为 List。枚举将有两个变体:Cons,它包含一个整数和一个 List,以及 Nil,表示 cons list 的结束。代码如下:

type List = Cons(integer, List) | Nil

// 这还不能编译!

等等,这段代码有一个问题:它无法编译!让我们看看为什么。如果我们尝试编译这段代码,我们会得到一个错误,说有一个“递归类型没有无限大小“。

为什么会这样?让我们想想为什么。要弄清楚 List 类型需要多少空间,X 语言会查看每个变体,看看哪个变体需要最多空间。X 语言看到 Cons 变体包含一个 integer 和一个 List,这意味着 Cons 需要 integer 的大小加上 List 的大小。为了弄清楚 List 需要多少空间,它查看它的变体,从 Cons 变体开始,依此类推,无限循环。

为了修复这个错误,我们需要给 X 语言一个关于 List 有多大的提示,通过打破递归,在其中一个变体中插入一个 box。因为 box 是一个指针,我们总是知道它需要多少空间:指针的大小不会根据它指向的数据量而变化。这意味着我们可以在 Cons 变体中放一个 Box,它指向的是下一个 List 值,而不是直接放另一个 List 值。从概念上讲,我们仍然有一个“包含“其他列表的列表,但现在这种表示是通过将 Box 放在下一个 List 前面,而不是直接包含它来实现的。

这是我们的修改:

type List = Cons(integer, Box<List>) | Nil

现在 Cons 变体将需要 integer 的大小加上 box 的大小。Nil 变体不存储任何值,所以它需要的空间比 Cons 变体少。现在我们知道 List 枚举最大需要 integer 的大小加上 box 的大小。通过使用 box,我们打破了无限递归链,因此 X 语言可以计算出存储 List 值需要多大的大小。

box 只是间接和堆分配;它们没有任何其他特殊功能,比如我们将在本章后面看到的其他智能指针。它们也没有 DerefDrop trait 提供的功能,所以我们将通过查看这些 trait 以及我们如何在自定义智能指针上使用它们来继续!