智能指针
- 指针 (pointer)是一个包含内存地址的变量的通用概念。这个地址引用,或 “指向”(points at)一些其他数据。Rust 中最常见的指针是引用(reference)。引用以
&
符号为标志并借用了它们所指向的值。除了引用数据没有任何其他特殊功能。它们也没有任何额外开销,所以应用得最多。 - 智能指针(smart pointers)是一类数据结构,它们的表现类似指针,但是也拥有额外的元数据和功能。智能指针的概念并非
Rust
独有:其起源于C++
,也存在于其他语言中。Rust 标准库中不同的智能指针提供了多于引用的额外功能。本章将会探索的一个例子便是 引用计数 (reference counting
)智能指针类型,其允许数据有多个所有者。引用计数智能指针记录总共有多少个所有者,并当没有任何所有者时负责清理数据。 - 在
Rust
中,普通引用和智能指针的一个额外的区别是引用是一类只借用数据的指针;相反,在大部分情况下,智能指针 拥有 它们指向的数据。 -
String
和Vec<T>
,虽然当时我们并不这么称呼它们。这些类型都属于智能指针因为它们拥有一些数据并允许你修改它们。它们也带有元数据(比如它们的容量)和额外的功能或保证(String
的数据总是有效的UTF-8
编码)。 - 智能指针通常使用结构体实现。智能指针区别于常规结构体的显著特性在于其实现了
Deref
和Drop trait
。Deref trait
允许智能指针结构体实例表现的像引用一样,这样就可以编写既用于引用、又用于智能指针的代码。Drop trait
允许我们自定义当智能指针离开作用域时运行的代码。本章会讨论这些 trait 以及为什么对于智能指针来说它们很重要。 - 考虑到智能指针是一个在
Rust
经常被使用的通用设计模式,本章并不会覆盖所有现存的智能指针。很多库都有自己的智能指针而你也可以编写属于你自己的智能指针。这里将会讲到的是来自标准库中最常用的一些:
-
Box<T>
,用于在堆上分配值 -
Rc<T>
,一个引用计数类型,其数据可以有多个所有者 -
Ref<T>
和RefMut<T>
,通过RefCell<T>
访问(RefCell<T>
是一个在运行时而不是在编译时执行借用规则的类型)
- 另外我们会涉及 内部可变性(interior mutability)模式,这是不可变类型暴露出改变其内部值的 API。我们也会讨论 引用循环(reference cycles)会如何泄漏内存,以及如何避免。
使用Box指向堆上的数据
栈(Stack)与堆(Heap)
在很多语言中,你并不需要经常考虑到栈与堆。不过在像 Rust 这样的系统编程语言中,值是位于栈上还是堆上在更大程度上影响了语言的行为以及为何必须做出这样的抉择。我们会在本章的稍后部分描述所有权与栈和堆相关的内容,所以这里只是一个用来预热的简要解释。
栈和堆都是代码在运行时可供使用的内存,但是它们的结构不同。栈以放入值的顺序存储值并以相反顺序取出值。这也被称作 后进先出(last in, first out)。想象一下一叠盘子:当增加更多盘子时,把它们放在盘子堆的顶部,当需要盘子时,也从顶部拿走。不能从中间也不能从底部增加或拿走盘子!增加数据叫做 进栈(pushing onto the stack),而移出数据叫做 出栈(popping off the stack)。
栈中的所有数据都必须占用已知且固定的大小。在编译时大小未知或大小可能变化的数据,要改为存储在堆上。堆是缺乏组织的:当向堆放入数据时,你要请求一定大小的空间。内存分配器(memory allocator)在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的 指针(pointer)。这个过程称作 在堆上分配内存(allocating on the heap),有时简称为 “分配”(allocating)。将数据推入栈中并不被认为是分配。因为指针的大小是已知并且固定的,你可以将指针存储在栈上,不过当需要实际数据时,必须访问指针。
想象一下去餐馆就座吃饭。当进入时,你说明有几个人,餐馆员工会找到一个够大的空桌子并领你们过去。如果有人来迟了,他们也可以通过询问来找到你们坐在哪。
入栈比在堆上分配内存要快,因为(入栈时)分配器无需为存储新数据去搜索内存空间;其位置总是在栈顶。相比之下,在堆上分配内存则需要更多的工作,这是因为分配器必须首先找到一块足够存放数据的内存空间,并接着做一些记录为下一次分配做准备。
访问堆上的数据比访问栈上的数据慢,因为必须通过指针来访问。现代处理器在内存中跳转越少就越快(缓存)。继续类比,假设有一个服务员在餐厅里处理多个桌子的点菜。在一个桌子报完所有菜后再移动到下一个桌子是最有效率的。从桌子 A 听一个菜,接着桌子 B 听一个菜,然后再桌子 A,然后再桌子 B 这样的流程会更加缓慢。出于同样原因,处理器在处理的数据彼此较近的时候(比如在栈上)比较远的时候(比如可能在堆上)能更好的工作。在堆上分配大量的空间也可能消耗时间。
当你的代码调用一个函数时,传递给函数的值(包括可能指向堆上数据的指针)和函数的局部变量被压入栈中。当函数结束时,这些值被移出栈。
跟踪哪部分代码正在使用堆上的哪些数据,最大限度的减少堆上的重复数据的数量,以及清理堆上不再使用的数据确保不会耗尽空间,这些问题正是所有权系统要处理的。一旦理解了所有权,你就不需要经常考虑栈和堆了,不过明白了所有权的存在就是为了管理堆数据,能够帮助解释为什么所有权要以这种方式工作。
- 最简单直接的智能指针是
box
,其类型是Box<T>
。box
允许你将一个值放在堆上而不是栈上。留在栈上的则是指向堆数据的指针。 - 除了数据被储存在堆上而不是栈上之外,
box
没有性能损失。不过也没有很多额外的功能。它们多用于如下场景:
- 当有一个在编译时未知大小的类型,而又想要在需要确切大小的上下文中使用这个类型值的时候
- 当有大量数据并希望在确保数据不被拷贝的情况下转移所有权的时候
- 当希望拥有一个值并只关心它的类型是否实现了特定 trait 而不是其具体类型的时候
- 我们会在 “box 允许创建递归类型” 部分展示第一种场景。在第二种情况中,转移大量数据的所有权可能会花费很长的时间,因为数据在栈上进行了拷贝。为了改善这种情况下的性能,可以通过
box
将这些数据储存在堆上。接着,只有少量的指针数据在栈上被拷贝。第三种情况被称为trait
对象(trait object)
使用Box在堆上储存数据
- 在讨论
Box<T>
的用例之前,让我们熟悉一下语法以及如何与储存在Box<T>
中的值进行交互。 - 示例展示了如何使用
box
在堆上储存一个i32
:
fn main() {
let b = Box::new(5);
println!("b = {}", b);
}
- 这里定义了变量
b
,其值是一个指向被分配在堆上的值 5 的 Box。这个程序会打印出b = 5
;在这个例子中,我们可以像数据是储存在栈上的那样访问box
中的数据。正如任何拥有数据所有权的值那样,当像b
这样的box
在main
的末尾离开作用域时,它将被释放。这个释放过程作用于box
本身(位于栈上)和它所指向的数据(位于堆上)。 - 将一个单独的值存放在堆上并不是很有意义,所以像示例这样的
box
并不常见,将像单个i32
这样的值储存在栈上,也就是其默认存放的地方在大部分使用场景中更为合适。让我们看看一个不使用box
时无法定义的类型的例子。
Box允许创建递归类型
- Rust 需要在编译时知道类型占用多少空间。一种无法在编译时知道大小的类型是 递归类型(recursive type),其值的一部分可以是相同类型的另一个值。这种值的嵌套理论上可以无限的进行下去,所以
Rust
不知道递归类型需要多少空间。不过box
有一个已知的大小,所以通过在循环类型定义中插入box
,就可以创建递归类型了。 - 让我们探索一下
cons list
,一个函数式编程语言中的常见类型,来展示这个(递归类型)概念。除了递归之外,我们将要定义的cons list
类型是很直白的,所以这个例子中的概念,在任何遇到更为复杂的涉及到递归类型的场景时都很实用。
cons list的更多内容
-
cons list
是一个来源于 Lisp 编程语言及其方言的数据结构。在 Lisp 中,cons
函数(“construct function
" 的缩写)利用两个参数来构造一个新的列表,他们通常是一个单独的值和另一个列表。 -
cons
函数的概念涉及到更常见的函数式编程术语;“将x
与y
连接” 通常意味着构建一个新的容器而将x
的元素放在新容器的开头,其后则是容器y
的元素。 -
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,
}
- 注意:出于示例的需要我们选择实现一个只存放
i32
值的cons list
。也可以用泛型来定义一个可以存放任何类型值的cons list
类型。 - 使用这个
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
是另一个包含 2 的Cons
值和下一个List
值。接着又有另一个存放了 3 的Cons
值和最后一个值为Nil
的List
,非递归成员代表了列表的结尾。 - 如果尝试编译,会得到下面的错误:
error[E0072]: recursive type `List` has infinite size
--> src/main.rs:1:1
|
1 | enum List {
| ^^^^^^^^^ recursive type has infinite size
2 | Cons(i32, List),
| ----- recursive without indirection
|
= help: insert indirection (e.g., a `Box`, `Rc`, or `&`) at some point to
make `List` representable
- 这个错误表明这个类型 “有无限的大小”。其原因是
List
的一个成员被定义为是递归的:它直接存放了另一个相同类型的值。这意味着Rust
无法计算为了存放List
值到底需要多少空间。让我们一点一点来看:首先了解一下Rust
如何决定需要多少空间来存放一个非递归类型。
计算非递归类型的大小
- 回忆下枚举定义时候的
Message
枚举:
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
- 当 Rust 需要知道要为
Message
值分配多少空间时,它可以检查每一个成员并发现Message::Quit
并不需要任何空间,Message::Move
需要足够储存两个i32
值的空间,依此类推。因为只会使用一个成员,所以Message
值需要的最大空间是存储其最大成员所需的空间大小。 - 与此相对应,当Rust编译器像检查示例中的
List
这样的递归类型时会发生什么呢,编译器尝试计算出储存一个List枚举需要多少内存,并开始检查Cons
成员,那么Cons
需要的空间等于i32
的大小加上List
的大小。为了计算List
需要多少内存,它检查其成员,从Cons
成员开始。Cons
成员储存了一个i32
值和一个List
值,这样的计算将无限进行下去,如图所示:
使用 Box 给递归类型一个已知的大小
Rust
无法计算出要为定义为递归的类型分配多少空间,所以编译器给出了示例中的错误。这个错误也包括了有用的建议:
= help: insert indirection (e.g., a `Box`, `Rc`, or `&`) at some point to
make `List` representable
- 在建议中,
“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
值需要的大小了。图展示了现在Cons
成员看起来像什么:box
只提供了间接存储和堆分配;他们并没有任何其他特殊的功能,比如我们将会见到的其他智能指针。它们也没有这些特殊功能带来的性能损失,所以他们可以用于像cons list
这样间接存储是唯一所需功能的场景。Box<T>
类型是一个智能指针,因为它实现了Deref trait
,它允许Box<T>
值被当作引用对待。当Box<T>
值离开作用域时,由于Box<T>
类型Drop trait
的实现,box
所指向的堆数据也会被清除。让我们更详细的探索一下这两个trait
。
- 实现
Deref trait
允许我们重载 解引用运算符(dereference operator)*
(与乘法运算符或通配符相区别)。通过这种方式实现 Deref trait 的智能指针可以被当作常规引用来对待,可以编写操作引用的代码并用于智能指针。 - 让我们首先看看解引用运算符如何处理常规引用,接着尝试定义我们自己的类似
Box<T>
的类型并看看为何解引用运算符不能像引用一样工作。我们会探索如何实现Deref trait
使得智能指针以类似引用的方式工作变为可能。最后,我们会讨论Rust
的 解引用强制转换(deref coercions
)功能以及它是如何处理引用或智能指针的。
我们将要构建的 MyBox 类型与真正的 Box 有一个很大的区别:我们的版本不会在堆上储存数据。这个例子重点关注 Deref,所以其数据实际存放在何处,相比其类似指针的行为来说不算重要。
通过解引用运算符追踪指针的值
- 常规引用是一种指针类型,一种理解指针的方式是将其看成指向储存在其他某处值的箭头。示例中创建了一个
i32
值的引用,接着使用解引用运算符解出所引用的值:
fn main() {
let x = 5;
let y = &x;
assert_eq!(5, x);
assert_eq!(5, *y);
}
- 变量
x
存放了一个i32
值 5。y
等于x
的一个引用。可以断言x
等于5
。然而,如果希望对y
的值做出断言,必须使用*y
来解出引用所指向的值(也就是 解引用)。一旦解引用了y
,就可以访问y
所指向的整型值并可以与5
做比较。 - 相反如果尝试编写
assert_eq!(5, y);
,则会得到如下编译错误:
error[E0277]: can't compare `{integer}` with `&{integer}`
--> src/main.rs:6:5
|
6 | assert_eq!(5, y);
| ^^^^^^^^^^^^^^^^^ no implementation for `{integer} == &{integer}`
|
= help: the trait `std::cmp::PartialEq<&{integer}>` is not implemented for
`{integer}`
- 不允许比较数字的引用与数字,因为它们是不同的类型。必须使用解引用运算符解出引用所指向的值。
像引用一样使用Box
- 可以使用 Box 代替引用来重写示例中的代码,解引用运算符也一样能工作.
fn main() {
let x = 5;
let y = Box::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
- 与上段代码唯一不同的地方就是将
y
设置为一个指向x
值的box
实例,而不是指向x
值的引用。在最后的断言中,可以使用解引用运算符以y
为引用时相同的方式追踪box
的指针。接下来让我们通过实现自己的box
类型来探索Box<T>
能这么做有何特殊之处。
自定义智能指针
- 为了体会默认情况下智能指针与引用的不同,让我们创建一个类似于标准库提供的
Box<T>
类型的智能指针。接着学习如何增加使用解引用运算符的功能。 - 从根本上说,
Box<T>
被定义为包含一个元素的元组结构体,所以示例以相同的方式定义了MyBox<T>
类型。我们还定义了new
函数来对应定义于Box<T>
的new
函数:
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
- 这里定义了一个结构体
MyBox
并声明了一个泛型参数T
,因为我们希望其可以存放任何类型的值。MyBox
是一个包含T
类型元素的元组结构体。MyBox::new
函数获取一个T
类型的参数并返回一个存放传入值的MyBox
实例。 - 尝试将示例代码加入示例中,并修改
main
函数使用我们定义的MyBox<T>
类型代替Box<T>,
示例中代码不能够编译,因为Rust不知道如何解引用MyBox
。
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
- 示例中得到的编译错误是:
error[E0614]: type `MyBox<{integer}>` cannot be dereferenced
--> src/main.rs:14:19
|
14 | assert_eq!(5, *y);
| ^^
MyBox<T>
类型不能解引用,因为我们尚未在该类型实现这个功能。为了启用*
运算符的解引用功能,需要实现Deref trait
。
通过实现Deref trait将某一类型像引用一样处理
- 为了实现
trait
,需要提供trait
所需的方法实现。Deref trait
,由标准库提供,要求实现名为deref
的方法,其借用self
并返回一个内部数据的引用。
use std::ops::Deref;
struct MyBox<T>(T);
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
type Target = T;
语法定义了用于此trait
的关联类型。关联类型是一个稍有不同的定义泛型参数的方式deref
方法体中写入了&self.0
,这样deref
返回了我希望通过*
运算符访问的值的引用。- 没有
Deref trait
的话,编译器只会解引用&
引用类型。deref
方法向编译器提供了获取任何实现了Deref trait
的类型的值,并且调用这个类型的deref
方法来获取一个它知道如何解引用的&
引用的能力。 - 当输入*y的时候实际上输入的是:
*(y.deref())
Rust
将*
运算符替换为先调用deref
方法再进行普通解引用的操作,如此我们便不用担心是否还需手动调用deref
方法了。Rust
的这个特性可以让我们写出行为一致的代码,无论是面对的是常规引用还是实现了Deref
的类型。deref
方法返回了一个值的引用,而*(y.deref())
括号外边的普通解引用仍然必须存在的原因是因为所有权。如果deref
方法直接返回值而不是值的引用,其值(的所有权)将被移出self
。在这里以及大部分使用解引用运算符的情况下,我们并不希望获取MyBox<T>
内部值的所有权。- 注意,每次当我们在代码中使用
*
时,*
运算符都被替换成了先调用deref
方法再接着使用*
解引用的操作,且只会发生一次,不会对*
操作符无限递归替换,解引用出上面i32
类型的值就停止了,这个值与示例中assert_eq!
的5
相匹配。
函数和方法的隐式解引用强制转换
- 解引用强制转换(
deref coercions
)是Rust
在函数或方法传参上的一种便利。解引用强制转换只能工作在实现了Deref trait
的类型上。解引用强制转换将一种类型(A
)隐式转换为另外一种类型(B
)的引用,因为A
类型实现了Deref trait
,并且其关联类型是B
类型。 - 比如,解引用强制转换可以将
&String
转换为&str
,因为类型String
实现了Deref trait
并且其关联类型是str
。
#[stable(feature = "rust1", since = "1.0.0")]
impl ops::Deref for String {
type Target = str;
#[inline]
fn deref(&self) -> &str {
unsafe { str::from_utf8_unchecked(&self.vec) }
}
}
- 当我们将特定类型的值的引用作为参数传递给函数或方法,但是被传递的值的引用与函数或方法中定义的参数类型不匹配时,会发生解引用强制转换。这时会有一系列的
deref
方法被调用,把我们提供的参数类型转换成函数或方法需要的参数类型。 - 解引用强制转换的加入使得
Rust
开发者编写函数和方法调用时无需增加过多显式使用&
和*
的引用和解引用。这个功能也使得我们可以编写更多同时作用于引用或智能指针的代码。 - 作为展示解引用强制转换的实例,让我们使用示例中定义的
MyBox<T>
,以及示例中增加的Deref
实现。示例展示了一个有着字符串slice
参数的函数定义:
fn hello(name: &str) {
println!("Hello, {}!", name);
}
- 可以使用字符串
slice
作为参数调用hello
函数,比如hello("Rust");
。解引用强制转换使得用MyBox<String>
类型值的引用调用hello
成为可能,
use std::ops::Deref;
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
fn hello(name: &str) {
println!("Hello, {}!", name);
}
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&m);
}
- 这里使用
&m
调用hello
函数,其为MyBox<String>
值的引用。因为示例中在MyBox<T>
上实现了Deref trait
,Rust
可以通过deref
调用将&MyBox<String>
变为&String
。标准库中提供了String
上的Deref
实现,其会返回字符串slice
,这可以在Deref
的API
文档中看到。Rust
再次调用deref
将&String
变为&str
,这就符合hello
函数的定义了。 - 如果 Rust 没有实现解引用强制转换,为了使用
&MyBox<String>
类型的值调用hello
,则不得不编写示例中的代码来代替:
use std::ops::Deref;
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
fn hello(name: &str) {
println!("Hello, {}!", name);
}
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&(*m)[..]);
}
(*m)
将MyBox<String>
解引用为String
。接着&
和[..]
获取了整个String
的字符串slice
来匹配hello
的签名。没有解引用强制转换所有这些符号混在一起将更难以读写和理解。解引用强制转换使得Rust
自动的帮我们处理这些转换。- 注意运算符的优先级和结合性,先为
*m
再[..]
最后&
。 - 当所涉及到的类型定义了
Deref trait
,Rust 会分析这些类型并使用任意多次Deref::deref
调用以获得匹配参数的类型。这些解析都发生在编译时,所以利用解引用强制转换并没有运行时损耗
解引用强制转换如何与可变性交互
- 类似于使用
Deref trait
重载不可变引用的*
运算符,Rust 提供了DerefMut trait
用于重载可变引用的*
运算符。 - Rust 在发现类型和
trait
实现满足三种情况时会进行解引用强制转换: - 当
T:
Deref<Target=U>
时从&T
到&U
。 - 当
T:
DerefMut<Target=U>
时从&mut T
到&mut U
。 - 当
T:
Deref<Target=U>
时从&mut T
到&U
- 头两个情况除了可变性之外是相同的:第一种情况表明如果有一个
&T
,而 T 实现了返回 U 类型的Deref
,则可以直接得到&U
。第二种情况表明对于可变引用也有着相同的行为。 - 第三个情况有些微妙:
Rust
也会将可变引用强转为不可变引用。但是反之是 不可能 的:不可变引用永远也不能强转为可变引用。因为根据借用规则,如果有一个可变引用,其必须是这些数据的唯一引用(否则程序将无法编译)。将一个可变引用转换为不可变引用永远也不会打破借用规则。将不可变引用转换为可变引用则需要数据只能有一个不可变引用,而借用规则无法保证这一点。因此,Rust
无法假设将不可变引用转换为可变引用是可能的。
- 对于智能指针模式来说第二个重要的
trait
是Drop
,其允许我们在值要离开作用域时执行一些代码。可以为任何类型提供Drop trait
的实现,同时所指定的代码被用于释放类似于文件或网络连接的资源。我们在智能指针上下文中讨论Drop
是因为其功能几乎总是用于实现智能指针。例如,Box<T>
自定义了Drop
用来释放box
所指向的堆空间。 - 在其他一些语言中,我们不得不记住在每次使用完智能指针实例后调用清理内存或资源的代码。如果忘记的话,运行代码的系统可能会因为负荷过重而崩溃。在
Rust
中,可以指定每当值离开作用域时被执行的代码,编译器会自动插入这些代码。于是我们就不需要在程序中到处编写在实例结束时清理这些变量的代码 —— 而且还不会泄漏资源。 - 指定在值离开作用域时应该执行的代码的方式是实现
Drop trait
。Drop trait
要求实现一个叫做drop
的方法,它获取一个self
的可变引用。为了能够看出Rust
何时调用drop
,让我们暂时使用println!
语句实现drop
。 - 示例展示了唯一定制功能就是当其离开作用域时,打印出
Dropping CustomSmartPointer!
的结构体CustomSmartPointer
。这回演示Rust
何时运行drop
函数:
struct CustomSmartPointer {
data: String,
}
impl Drop for CustomSmartPointer {
fn drop(&mut self) {
println!("Dropping CustomSmartPointer with data `{}`!", self.data);
}
}
fn main() {
let c = CustomSmartPointer { data: String::from("my stuff") };
let d = CustomSmartPointer { data: String::from("other stuff") };
println!("CustomSmartPointers created.");
}
Drop trait
包含在prelude
中,所以无需导入它。我们在CustomSmartPointer
上实现了Drop trait
,并提供了一个调用println!
的drop
方法实现。drop
函数体是放置任何当类型实例离开作用域时期望运行的逻辑的地方。这里选择打印一些文本以展示Rust
何时调用drop
。- 在
main
中,我们新建了两个CustomSmartPointer
实例并打印出了CustomSmartPointer created.
。在main
的结尾,CustomSmartPointer
的实例会离开作用域,而Rust
会调用放置于drop
方法中的代码,打印出最后的信息。注意无需显式调用drop
方法: - 运行程序得出如下输出:
CustomSmartPointers created.
Dropping CustomSmartPointer with data `other stuff`!
Dropping CustomSmartPointer with data `my stuff`!
- 当实例离开作用域
Rust
会自动调用drop
,并调用我们指定的代码。变量以被创建时相反的顺序被丢弃,所以d
在c
之前被丢弃。这个例子刚好给了我们一个drop
方法如何工作的可视化指导,不过通常需要指定类型所需执行的清理代码而不是打印信息。
通过std::men::drop提早丢弃值
- 不幸的是,我们并不能直截了当的禁用
drop
这个功能。通常也不需要禁用drop
;整个Drop trait
存在的意义在于其是自动处理的。然而,有时你可能需要提早清理某个值。一个例子是当使用智能指针管理锁时;你可能希望强制运行drop
方法来释放锁以便作用域中的其他代码可以获取锁。Rust 并不允许我们主动调用Drop trait
的drop
方法;当我们希望在作用域结束之前就强制释放变量的话,我们应该使用的是由标准库提供的std::mem::drop
。 - 像上个示例那样调用
Drop trait
的drop
方法,就会得到示例那样的错误:
fn main() {
let c = CustomSmartPointer { data: String::from("some data") };
println!("CustomSmartPointer created.");
c.drop();
println!("CustomSmartPointer dropped before the end of main.");
}
- 如果尝试编译代码,会出现下面的错误:
error[E0040]: explicit use of destructor method
--> src/main.rs:14:7
|
14 | c.drop();
| ^^^^ explicit destructor calls not allowed
- 错误信息表明不允许显式调用
drop
。错误信息使用了术语 析构函数(destructor
),这是一个清理实例的函数的通用编程概念。析构函数 对应创建实例的 构造函数。Rust
中的drop
函数就是这么一个析构函数。 Rust
不允许我们显式调用drop
因为Rust
仍然会在main
的结尾对值自动调用drop
,这会导致一个double free
错误,因为Rust
会尝试清理相同的值两次。- 因为不能禁用当值离开作用域时自动插入的
drop
,并且不能显式调用drop
,如果我们需要强制提早清理值,可以使用std::mem::drop
函数。 std::mem::drop
函数不同于Drop trait
中的drop
方法。可以通过传递希望提早强制丢弃的值作为参数。std::mem::drop
位于prelude
,修改示例调用main
函数来使用drop
函数:
struct CustomSmartPointer {
data: String,
}
impl Drop for CustomSmartPointer {
fn drop(&mut self) {
println!("Dropping CustomSmartPointer with data `{}`!", self.data);
}
}
fn main() {
let c = CustomSmartPointer { data: String::from("some data") };
println!("CustomSmartPointer created.");
drop(c);
println!("CustomSmartPointer dropped before the end of main.");
}
- 运行这段代码,打印出下面的值:
CustomSmartPointer created.
Dropping CustomSmartPointer with data `some data`!
CustomSmartPointer dropped before the end of main.
Dropping CustomSmartPointer with data
some data!
出现在CustomSmartPointer created.
和CustomSmartPointer dropped before the end of main.
之间,表明了drop
方法被调用了并在此丢弃了c
。Drop trait
实现中指定的代码可以用于许多方面,来使得清理变得方便和安全:比如可以用其创建我们自己的内存分配器!通过Drop trait
和Rust
所有权系统,你无需担心之后的代码清理,Rust
会自动考虑这些问题。- 我们也无需担心意外的清理掉仍在使用的值,这会造成编译器错误:所有权系统确保引用总是有效的,也会确保
drop
只会在值不再被使用时被调用一次。
- 大部分情况下所有权是非常明确的:可以准确地知道哪个变量拥有某个值。然而,有些情况单个值可能会有多个所有者。例如,在图数据结构中,多个边可能指向相同的节点,而这个节点从概念上讲为所有指向它的边所拥有。节点直到没有任何边指向它之前都不应该被清理。
- 为了启用多所有权,
Rust
有一个叫做Rc<T>
的类型。其名称为 引用计数(reference counting
)的缩写。引用计数意味着记录一个值引用的数量来知晓这个值是否仍在被使用。如果某个值有零个引用,就代表没有任何有效引用并可以被清理。 - 可以将其想象为客厅中的电视。当一个人进来看电视时,他打开电视。其他人也可以进来看电视。当最后一个人离开房间时,他关掉电视因为它不再被使用了。如果某人在其他人还在看的时候就关掉了电视,正在看电视的人肯定会抓狂的!
Rc<T>
用于当我们希望在堆上分配一些内存供程序的多个部分读取,而且无法在编译时确定程序的哪一部分会最后结束使用它的时候。如果确实知道哪部分是最后一个结束使用的话,就可以令其成为数据的所有者,正常的所有权规则就可以在编译时生效。
注意 Rc 只能用于单线程场景
使用Rc共享数据
- 让我们回到示例中使用
Box<T>
定义cons list
的例子。这一次,我们希望创建两个共享第三个列表所有权的列表,其概念将会看起来如图所示: - 列表 a 包含 5 之后是 10,之后是另两个列表:b 从 3 开始而 c 从 4 开始。b 和 c 会接上包含 5 和 10 的列表 a。换句话说,这两个列表会尝试共享第一个列表所包含的 5 和 10。
- 尝试使用
Box<T>
定义的List
实现并不能工作:
enum List {
Cons(i32, Box<List>),
Nil,
}
use crate::List::{Cons, Nil};
fn main() {
let a = Cons(5,
Box::new(Cons(10,
Box::new(Nil))));
let b = Cons(3, Box::new(a));
let c = Cons(4, Box::new(a));
}
- 编译会得出如下错误:
error[E0382]: use of moved value: `a`
--> src/main.rs:13:30
|
12 | let b = Cons(3, Box::new(a));
| - value moved here
13 | let c = Cons(4, Box::new(a));
| ^ value used here after move
|
= note: move occurs because `a` has type `List`, which does not implement
the `Copy` trait
Cons
成员拥有其储存的数据,所以当创建b
列表时,a
被移动进了b
这样b
就拥有了a
。接着当再次尝试使用a
创建c
时,这不被允许,因为a
的所有权已经被移动。- 可以改变
Cons
的定义来存放一个引用,不过接着必须指定生命周期参数。通过指定生命周期参数,表明列表中的每一个元素都至少与列表本身存在的一样久。例如,借用检查器不会允许let a = Cons(10, &Nil);
编译,因为临时值Nil
会在a
获取其引用之前就被丢弃了。 - 相反,我们修改
List
的定义为使用Rc<T>
代替Box<T>
,如列表所示,现在每一个Cons
变量都包含一个值和一个指向List
的Rc<T>
,当创建b
的时候,不同于获取a
的所有权,这里会克隆a
所包含的Rc<List>
,这会将引用计数从1
增加到2
并允许a
和b
共享Rc<List>
中数据的所有权。创建c
时也会克隆a
,这会将引用计数从2
增加为3
。每次调用Rc::clone
,Rc<List>
中数据的引用计数都会增加,直到有零个引用之前其数据都不会被清理。
enum List {
Cons(i32, Rc<List>),
Nil,
}
use crate::List::{Cons, Nil};
use std::rc::Rc;
fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
let b = Cons(3, Rc::clone(&a));
let c = Cons(4, Rc::clone(&a));
}
- 需要使用
use
语句将Rc<T>
引入作用域,因为它不在prelude
中。在main
中创建了存放5
和10
的列表并将其存放在a
的新的Rc<List>
中。接着当创建b
和c
时,调用Rc::clone
函数并传递a
中Rc<List>
的引用作为参数。 - 也可以调用
a.clone()
而不是Rc::clone(&a)
,不过在这里 Rust 的习惯是使用Rc::clone
。Rc::clone
的实现并不像大部分类型的clone
实现那样对所有数据进行深拷贝。Rc::clone
只会增加引用计数,这并不会花费多少时间。深拷贝可能会花费很长时间。通过使用Rc::clone
进行引用计数,可以明显的区别深拷贝类的克隆和增加引用计数类的克隆。当查找代码中的性能问题时,只需考虑深拷贝类的克隆而无需考虑Rc::clone
调用。
克隆Rc会增加引用计数
- 修改之前的代码以观察创建和丢弃
a
中的Rc<List>
引用时引用计数的变化 - 修改了
main
以便将列表c
置于内部作用域中,这样就可以观察当c
离开作用域时引用计数如何变化。
enum List {
Cons(i32, Rc<List>),
Nil,
}
use crate::List::{Cons, Nil};
use std::rc::Rc;
fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
println!("count after creating a = {}", Rc::strong_count(&a));
let b = Cons(3, Rc::clone(&a));
println!("count after creating b = {}", Rc::strong_count(&a));
{
let c = Cons(4, Rc::clone(&a));
println!("count after creating c = {}", Rc::strong_count(&a));
}
println!("count after c goes out of scope = {}", Rc::strong_count(&a));
}
- 在程序中每个引用计数变化的点,会打印出引用计数,其值可以通过调用
Rc::strong_count
函数获得。这个函数叫做strong_count
而不是count
是因为Rc<T>
也有weak_count
;在 “避免引用循环:将Rc<T>
变为Weak<T>
” 部分会讲解weak_count
的用途。 - 这时代码打印出:
count after creating a = 1
count after creating b = 2
count after creating c = 3
count after c goes out of scope = 2
- 我们能够看到
a
中Rc<List>
的初始引用计数为1
,接着每次调用clone
,计数会增加1
。当c
离开作用域时,计数减1
。不必像调用Rc::clone
增加引用计数那样调用一个函数来减少计数;Drop trait
的实现当Rc<T>
值离开作用域时自动减少引用计数。 - 从这个例子我们所不能看到的是,在
main
的结尾当b
然后是a
离开作用域时,此处计数会是0
,同时Rc<List>
被完全清理。使用Rc<T>
允许一个值有多个所有者,引用计数则确保只要任何所有者依然存在其值也保持有效。 - 通过不可变引用,
Rc<T>
允许在程序的多个部分之间只读地共享数据。如果Rc<T>
也允许多个可变引用,则会违反讨论的借用规则之一:相同位置的多个可变借用可能造成数据竞争和不一致。不过可以修改数据是非常有用的!在下一部分,我们将讨论内部可变性模式和 RefCell 类型,它可以与Rc<T>
结合使用来处理不可变性的限制。
- 内部可变性(
Interior mutability
)是Rust
中的一个设计模式,它允许你即使在有不可变引用时也可以改变数据,这通常是借用规则所不允许的。为了改变数据,该模式在数据结构中使用unsafe
代码来模糊Rust
通常的可变性和借用规则。当可以确保代码在运行时会遵守借用规则,即使编译器不能保证的情况,可以选择使用那些运用内部可变性模式的类型。所涉及的unsafe
代码将被封装进安全的API
中,而外部类型仍然是不可变的。 - 让我们通过遵循内部可变性模式的
RefCell<T>
类型来开始探索。
通过RefCell在运行时检查借用规则
- 不同于
Rc<T>
,RefCell<T>
代表其数据的唯一的所有权。那么是什么让RefCell<T>
不同于像Box<T>
这样的类型呢?在借用规则中: - 在任意给定时刻,只能拥有一个可变引用或任意数量的不可变引用 之一(而不是两者)。
- 引用必须总是有效的。
- 对于引用和
Box<T>
,借用规则的不可变性作用于编译时。对于RefCell<T>
,这些不可变性作用于 运行时。对于引用,如果违反这些规则,会得到一个编译错误。而对于RefCell<T>
,如果违反这些规则程序会panic
并退出。 - 在编译时检查借用规则的优势是这些错误将在开发过程的早期被捕获,同时对运行时没有性能影响,因为所有的分析都提前完成了。为此,在编译时检查借用规则是大部分情况的最佳选择,这也正是其为何是
Rust
的默认行为。 - 相反在运行时检查借用规则的好处则是允许出现特定内存安全的场景,而它们在编译时检查中是不允许的。静态分析,正如
Rust
编译器,是天生保守的。但代码的一些属性不可能通过分析代码发现:其中最著名的就是 停机问题(Halting Problem
). - 因为一些分析是不可能的,如果
Rust
编译器不能通过所有权规则编译,它可能会拒绝一个正确的程序;从这种角度考虑它是保守的。如果Rust
接受不正确的程序,那么用户也就不会相信Rust
所做的保证了。然而,如果Rust
拒绝正确的程序,虽然会给开发者带来不便,但不会带来灾难。RefCell<T>
正是用于当你确信代码遵守借用规则,而编译器不能理解和确定的时候。 - 类似于
Rc<T>
,RefCell<T>
只能用于单线程场景。如果尝试在多线程上下文中使用RefCell<T>
,会得到一个编译错误。 - 如下为选择
Box<T>
,Rc<T>
或RefCell<T>
的理由: Rc<T>
允许相同数据有多个所有者;Box<T>
和RefCell<T>
有单一所有者。Box<T>
允许在编译时执行不可变或可变借用检查;Rc<T>
仅允许在编译时执行不可变借用检查;RefCell<T>
允许在运行时执行不可变或可变借用检查。- 因为
RefCell<T>
允许在运行时执行可变借用检查,所以我们可以在即便RefCell<T>
自身是不可变的情况下修改其内部的值。 - 在不可变值内部改变值就是 内部可变性 模式。让我们看看何时内部可变性是有用的,并讨论这是如何成为可能的。
内部可变性:不可变值的可变借用
- 借用规则的一个推论是当有一个不可变值时,不能可变地借用它。例如,如下代码不能编译:
fn main() {
let x = 5;
let y = &mut x;
}
- 如果编译会出现错误:
error[E0596]: cannot borrow immutable local variable `x` as mutable
--> src/main.rs:3:18
|
2 | let x = 5;
| - consider changing this to `mut x`
3 | let y = &mut x;
| ^ cannot borrow mutably
- 然而,特定情况下,令一个值在其方法内部能够修改自身,而在其他代码中仍视为不可变,是很有用的。值方法外部的代码就不能修改其值了。
RefCell<T>
是一个获得内部可变性的方法。RefCell<T>
并没有完全绕开借用规则,编译器中的借用检查器允许内部可变性并相应地在运行时检查借用规则。如果违反了这些规则,会出现panic
而不是编译错误。 - 让我们通过一个实际的例子来探索何处可以使用
RefCell<T>
来修改不可变值并看看为何这么做是有意义的。
内部可变性的用例: mock对象
- 测试替身(
test double
)是一个通用编程概念,它代表一个在测试中替代某个类型的类型。mock
对象 是特定类型的测试替身,它们记录测试过程中发生了什么以便可以断言操作是正确的。 - 虽然
Rust
中的对象与其他语言中的对象并不是一回事,Rust
也没有像其他语言那样在标准库中内建mock
对象功能,不过我们确实可以创建一个与mock
对象有着相同功能的结构体。 - 如下是一个我们想要测试的场景:我们在编写一个记录某个值与最大值的差距的库,并根据当前值与最大值的差距来发送消息。例如,这个库可以用于记录用户所允许的
API
调用数量限额。 - 该库只提供记录与最大值的差距,以及何种情况发送什么消息的功能。使用此库的程序则期望提供实际发送消息的机制:程序可以选择记录一条消息、发送 email、发送短信等等。库本身无需知道这些细节;只需实现其提供的
Messenger trait
即可。 - src/lib.rs
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where T: Messenger {
pub fn new(messenger: &T, max: usize) -> LimitTracker<T> {
LimitTracker {
messenger,
value: 0,
max,
}
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger.send("Warning: You've used up over 75% of your quota!");
}
}
}
- 这些代码中一个重要部分是拥有一个方法
send
的Messenger trait
,其获取一个self
的不可变引用和文本信息。这是我们的mock
对象所需要拥有的接口。另一个重要的部分是我们需要测试LimitTracker
的set_value
方法的行为。可以改变传递的value
参数的值,不过set_value
并没有返回任何可供断言的值。也就是说,如果使用某个实现了Messenger trait
的值和特定的max
创建LimitTracker
,当传递不同value
值时,消息发送者应被告知发送合适的消息。 - 我们所需的
mock
对象是,调用send
并不实际发送 email 或消息,而是只记录信息被通知要发送了。可以新建一个mock
对象实例,用其创建LimitTracker
,调用LimitTracker
的set_value
方法,然后检查mock
对象是否有我们期望的消息。 - 示例展示了这样的
mock
对象实现,不过借用检查器并不允许:
#[cfg(test)]
mod tests {
use super::*;
struct MockMessenger {
sent_messages: Vec<String>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger { sent_messages: vec![] }
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages.push(String::from(message));
}
}
#[test]
fn it_sends_an_over_75_percent_warning_message() {
let mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
limit_tracker.set_value(80);
assert_eq!(mock_messenger.sent_messages.len(), 1);
}
}
- 测试代码定义了一个
MockMessenger
结构体,其sent_messages
字段为一个String
值的Vec
用来记录被告知发送的消息。我们还定义了一个关联函数new
以便于新建从空消息列表开始的MockMessenger
值。接着为MockMessenger
实现Messenger trait
这样就可以为LimitTracker
提供一个MockMessenger
。在send
方法的定义中,获取传入的消息作为参数并储存在MockMessenger
的sent_messages
列表中。 - 在测试中,我们测试了当
LimitTracker
被告知将value
设置为超过max
值75%
的某个值。首先新建一个MockMessenger
,其从空消息列表开始。接着新建一个LimitTracker
并传递新建MockMessenger
的引用和max
值100
。我们使用值80
调用LimitTracker
的set_value
方法,这超过了100
的75%
。接着断言MockMessenger
中记录的消息列表应该有一条消息。 - 但是,这个测试会出错,
error[E0596]: cannot borrow immutable field `self.sent_messages` as mutable
--> src/lib.rs:52:13
|
51 | fn send(&self, message: &str) {
| ----- use `&mut self` here to make mutable
52 | self.sent_messages.push(String::from(message));
| ^^^^^^^^^^^^^^^^^^ cannot mutably borrow immutable field
- 不能修改
MockMessenger
来记录消息,因为send
方法获取了self
的不可变引用。我们也不能参考错误文本的建议使用&mut self
替代,因为这样send
的签名就不符合Messenger trait
定义中的签名了(可以试着这么改,看看会出现什么错误信息)。 - 这正是内部可变性的用武之地!我们将通过
RefCell
来储存sent_messages
,然后send
将能够修改sent_messages
并储存消息。
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where T: Messenger {
pub fn new(messenger: &T, max: usize) -> LimitTracker<T> {
LimitTracker {
messenger,
value: 0,
max,
}
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger.send("Warning: You've used up over 75% of your quota!");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
struct MockMessenger {
sent_messages: RefCell<Vec<String>>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger { sent_messages: RefCell::new(vec![]) }
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages.borrow_mut().push(String::from(message));
}
}
#[test]
fn it_sends_an_over_75_percent_warning_message() {
// --snip--
let mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
limit_tracker.set_value(75);
assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
}
}
- 现在
sent_messages
字段的类型是RefCell<Vec<String>>
而不是Vec<String>
。在new
函数中新建了一个RefCell<Vec<String>>
实例替代空vector
。 - 对于
send
方法的实现,第一个参数仍为self
的不可变借用,这是符合方法定义的。我们调用self.sent_messages
中RefCell
的borrow_mut
方法来获取RefCell
中值的可变引用,这是一个vector
。接着可以对vector
的可变引用调用push
以便记录测试过程中看到的消息。 - 最后必须做出的修改位于断言中:为了看到其内部
vector
中有多少个项,需要调用RefCell
的borrow
以获取vector
的不可变引用。
RefCell在运行时记录借用
- 当创建不可变和可变引用时,我们分别使用 & 和 &mut 语法。对于
RefCell<T>
来说,则是borrow
和borrow_mut
方法,这属于RefCell<T>
安全 API 的一部分。borrow
方法返回Ref<T>
类型的智能指针,borrow_mut
方法返回RefMut
类型的智能指针。这两个类型都实现了Deref
,所以可以当作常规引用对待。 RefCell<T>
记录当前有多少个活动的Ref<T>
和RefMut<T>
智能指针。每次调用borrow
,RefCell<T>
将活动的不可变借用计数加一。当Ref<T>
值离开作用域时,不可变借用计数减一。就像编译时借用规则一样,RefCell<T>
在任何时候只允许有多个不可变借用或一个可变借用。- 如果我们尝试违反这些规则,相比引用时的编译时错误,
RefCell<T>
的实现会在运行时出现panic
。这里我们故意尝试在相同作用域创建两个可变借用以便演示RefCell<T>
不允许我们在运行时这么做:
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
let mut one_borrow = self.sent_messages.borrow_mut();
let mut two_borrow = self.sent_messages.borrow_mut();
one_borrow.push(String::from(message));
two_borrow.push(String::from(message));
}
}
- 这里为
borrow_mut
返回的RefMut
智能指针创建了one_borrow
变量。接着用相同的方式在变量two_borrow
创建了另一个可变借用。这会在相同作用域中创建两个可变引用,这是不允许的。当运行库的测试时,示例 15-23 编译时不会有任何错误,不过测试会失败:
---- tests::it_sends_an_over_75_percent_warning_message stdout ----
thread 'tests::it_sends_an_over_75_percent_warning_message' panicked at
'already borrowed: BorrowMutError', src/libcore/result.rs:906:4
note: Run with `RUST_BACKTRACE=1` for a backtrace.
- 注意代码
panic
和信息already borrowed: BorrowMutError
。这也就是RefCell<T>
如何在运行时处理违反借用规则的情况。 - 在运行时捕获借用错误而不是编译时意味着将会在开发过程的后期才会发现错误,甚至有可能发布到生产环境才发现;还会因为在运行时而不是编译时记录借用而导致少量的运行时性能惩罚。然而,使用
RefCell
使得在只允许不可变值的上下文中编写修改自身以记录消息的mock
对象成为可能。虽然有取舍,但是我们可以选择使用RefCell<T>
来获得比常规引用所能提供的更多的功能。
结合Rc和RefCell来拥有多个可变数据所有者
RefCell<T>
的一个常见用法是与Rc<T>
结合。回忆一下Rc<T>
允许对相同数据有多个所有者,不过只能提供数据的不可变访问。如果有一个储存了RefCell<T>
的Rc<T>
的话,就可以得到有多个所有者 并且 可以修改的值了!- 例如,
cons list
的例子中使用Rc<T>
使得多个列表共享另一个列表的所有权。因为Rc<T>
只存放不可变值,所以一旦创建了这些列表值后就不能修改。让我们加入RefCell<T>
来获得修改列表中值的能力。
#[derive(Debug)]
enum List {
Cons(Rc<RefCell<i32>>, Rc<List>),
Nil,
}
use crate::List::{Cons, Nil};
use std::rc::Rc;
use std::cell::RefCell;
fn main() {
let value = Rc::new(RefCell::new(5));
let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));
let b = Cons(Rc::new(RefCell::new(6)), Rc::clone(&a));
let c = Cons(Rc::new(RefCell::new(10)), Rc::clone(&a));
*value.borrow_mut() += 10;
println!("a after = {:?}", a);
println!("b after = {:?}", b);
println!("c after = {:?}", c);
}
- 这里创建了一个
Rc<RefCell<i32>>
实例并储存在变量value
中以便之后直接访问。接着在a
中用包含value
的Cons
成员创建了一个List
。需要克隆value
以便a
和value
都能拥有其内部值5
的所有权,而不是将所有权从value
移动到a
或者让 a 借用 value。 - 我们将列表
a
封装进了Rc<T>
这样当创建列表b
和c
时,他们都可以引用a
。 - 一旦创建了列表
a、b
和c
,我们将value
的值加10
。为此对value
调用了borrow_mut
,这里使用了自动解引用功能来解引用Rc<T>
以获取其内部的RefCell<T>
值。borrow_mut
方法返回RefMut<T>
智能指针,可以对其使用解引用运算符并修改其内部值。 - 当我们打印出 a、b 和 c 时,可以看到他们都拥有修改后的值 15 而不是 5:
a after = Cons(RefCell { value: 15 }, Nil)
b after = Cons(RefCell { value: 6 }, Cons(RefCell { value: 15 }, Nil))
c after = Cons(RefCell { value: 10 }, Cons(RefCell { value: 15 }, Nil))
- 这是非常巧妙的!通过使用
RefCell<T>
,我们可以拥有一个表面上不可变的List
,不过可以使用RefCell<T>
中提供内部可变性的方法来在需要时修改数据。RefCell<T>
的运行时借用规则检查也确实保护我们免于出现数据竞争——有时为了数据结构的灵活性而付出一些性能是值得的。 - 标准库中也有其他提供内部可变性的类型,比如
Cell<T>
,它类似RefCell<T>
但有一点除外:它并非提供内部值的引用,而是把值拷贝进和拷贝出Cell<T>
。还有Mutex<T>
,其提供线程间安全的内部可变性
- Rust 的内存安全性保证使其难以意外地制造永远也不会被清理的内存(被称为 内存泄漏(
memory leak
)),但并不是不可能。与在编译时拒绝数据竞争不同,Rust
并不保证完全地避免内存泄漏,这意味着内存泄漏在 Rust 被认为是内存安全的。这一点可以通过Rc<T>
和RefCell<T>
看出:创建引用循环的可能性是存在的。这会造成内存泄漏,因为每一项的引用计数永远也到不了0
,其值也永远不会被丢弃。
制造引用循环
- 让我们看看引用循环是如何发生的以及如何避免它。以示例中的
List
枚举和tail
方法的定义开始: src/main.rs
use std::rc::Rc;
use std::cell::RefCell;
use crate::List::{Cons, Nil};
#[derive(Debug)]
enum List {
Cons(i32, RefCell<Rc<List>>),
Nil,
}
impl List {
fn tail(&self) -> Option<&RefCell<Rc<List>>> {
match self {
Cons(_, item) => Some(item),
Nil => None,
}
}
}
- 这里采用示例中的
List
定义的另一种变体。现在Cons
成员的第二个元素是RefCell<Rc<List>>
,这意味着不同于像示例那样能够修改i32
的值,我们希望能够修改Cons
成员所指向的List
。这里还增加了一个tail
方法来方便我们在有Cons
成员的时候访问其第二项。 - 示例中增加了一个
main
函数,这些代码在a
中创建了一个列表,一个指向a
中列表的b
列表,接着修改a
中的列表指向b
中的列表,这会创建一个引用循环。在这个过程的多个位置有println!
语句展示引用计数。 - src/main.rs
use crate::List::{Cons, Nil};
use std::rc::Rc;
use std::cell::RefCell;
#[derive(Debug)]
enum List {
Cons(i32, RefCell<Rc<List>>),
Nil,
}
impl List {
fn tail(&self) -> Option<&RefCell<Rc<List>>> {
match self {
Cons(_, item) => Some(item),
Nil => None,
}
}
}
fn main() {
let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));
println!("a initial rc count = {}", Rc::strong_count(&a));
println!("a next item = {:?}", a.tail());
let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));
println!("a rc count after b creation = {}", Rc::strong_count(&a));
println!("b initial rc count = {}", Rc::strong_count(&b));
println!("b next item = {:?}", b.tail());
if let Some(link) = a.tail() {
*link.borrow_mut() = Rc::clone(&b);
}
println!("b rc count after changing a = {}", Rc::strong_count(&b));
println!("a rc count after changing a = {}", Rc::strong_count(&a));
// Uncomment the next line to see that we have a cycle;
// it will overflow the stack
// println!("a next item = {:?}", a.tail());
}
- 这里在变量
a
中创建了一个Rc<List>
实例来存放初值为5
,Nil
的List
值。接着在变量b
中创建了存放包含值10
和指向列表a
的List
的另一个Rc<List>
实例。 - 最后,修改
a
使其指向b
而不是Nil
,这就创建了一个循环。为此需要使用tail
方法获取a
中RefCell<Rc<List>>
的引用,并放入变量link
中。接着使用RefCell<Rc<List>>
的borrow_mut
方法将其值从存放Nil
的Rc<List>
修改为b
中的Rc<List>
。 - 如果保持最后的println!行并注释运行代码,得到下面的输出:
a initial rc count = 1
a next item = Some(RefCell { value: Nil })
a rc count after b creation = 2
b initial rc count = 1
b next item = Some(RefCell { value: Cons(5, RefCell { value: Nil }) })
b rc count after changing a = 2
a rc count after changing a = 2
- 可以看到将列表
a
修改为指向b
之后,a
和b
中的Rc<List>
实例的引用计数都是2
。在main
的结尾,Rust
首先丢弃变量b
,这会使b
中Rc<List>
实例的引用计数减1
。然而,因为a
仍然引用b
中的Rc<List>
,Rc<List>
的引用计数是1
而不是0
,所以b
中的Rc<List>
在堆上的内存不会被丢弃。接下来 Rust 会丢弃a
,同理这会将a
中Rc<List>
实例的引用计数从2
减为1
。这个实例的内存也不能被丢弃,因为其他的Rc<List>
实例仍在引用它。这些列表的内存将永远保持未被回收的状态。为了更形象地展示,我们创建了一个如图所示的引用循环: - 如果取消最后
println!
的注释并运行程序,Rust
会尝试打印出a
指向b
指向a
这样的循环直到栈溢出。 - 这个特定的例子中,创建了引用循环之后程序立刻就结束了。这个循环的结果并不可怕。如果在更为复杂的程序中并在循环里分配了很多内存并占有很长时间,这个程序会使用多于它所需要的内存,并有可能压垮系统并造成没有内存可供使用。
- 创建引用循环并不容易,但也不是不可能。如果你有包含
Rc<T>
的RefCell<T>
值或类似的嵌套结合了内部可变性和引用计数的类型,请务必小心确保你没有形成一个引用循环;你无法指望Rust
帮你捕获它们。创建引用循环是一个程序上的逻辑bug
,你应该使用自动化测试、代码评审和其他软件开发最佳实践来使其最小化。 - 另一个解决方案是重新组织数据结构,使得一部分引用拥有所有权而另一部分没有。换句话说,循环将由一些拥有所有权的关系和一些无所有权的关系组成,只有所有权关系才能影响值是否可以被丢弃。在示例中,我们总是希望
Cons
成员拥有其列表,所以重新组织数据结构是不可能的。让我们看看一个由父节点和子节点构成的图的例子,观察何时是使用无所有权的关系来避免引用循环的合适时机。
避免引用循环: 将Rc变成Weak
- 到目前为止,我们已经展示了调用
Rc::clone
会增加Rc<T>
实例的strong_count
,和只在其strong_count
为0
时才会被清理的Rc<T>
实例。你也可以通过调用Rc::downgrade
并传递Rc<T>
实例的引用来创建其值的 弱引用(weak reference
)。调用Rc::downgrade
时会得到Weak<T>
类型的智能指针。不同于将Rc<T>
实例的strong_count
加1
,调用Rc::downgrade
会将weak_count
加1
。Rc<T>
类型使用weak_count
来记录其存在多少个Weak<T>
引用,类似于strong_count
。其区别在于weak_count
无需计数为 0 就能使Rc<T>
实例被清理。 - 强引用代表如何共享
Rc<T>
实例的所有权,但弱引用并不属于所有权关系。他们不会造成引用循环,因为任何弱引用的循环会在其相关的强引用计数为0
时被打断。 - 因为
Weak<T>
引用的值可能已经被丢弃了,为了使用Weak<T>
所指向的值,我们必须确保其值仍然有效。为此可以调用Weak<T>
实例的upgrade
方法,这会返回Option<Rc<T>>
。如果Rc<T>
值还未被丢弃,则结果是Some
;如果Rc<T>
已被丢弃,则结果是None
。因为upgrade
返回一个Option<T>
,我们确信Rust
会处理Some
和None
的情况,所以它不会返回非法指针。 - 我们会创建一个某项知道其子项和父项的树形结构的例子,而不是只知道其下一项的列表。
创建树形数据结构: 带有子节点的Node
- 在最开始,我们将会构建一个带有子节点的树。让我们创建一个用于存放其拥有所有权的
i32
值和其子节点引用的Node
:
use std::rc::Rc;
use std::cell::RefCell;
#[derive(Debug)]
struct Node {
value: i32,
children: RefCell<Vec<Rc<Node>>>,
}
- 我们希望能够
Node
拥有其子节点,同时也希望通过变量来共享所有权,以便可以直接访问树中的每一个Node
,为此Vec<T>
的项的类型被定义为Rc<Node>
。我们还希望能修改其他节点的子节点,所以children
中Vec<Rc<Node>>
被放进了RefCell<T>
。 - 接下来,使用此结构体定义来创建一个叫做 leaf 的带有值 3 且没有子节点的 Node 实例,和另一个带有值 5 并以 leaf 作为子节点的实例 branch,如示例所示:
fn main() {
let leaf = Rc::new(Node {
value: 3,
children: RefCell::new(vec![]),
});
let branch = Rc::new(Node {
value: 5,
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
}
- 这里克隆了
leaf
中的Rc<Node>
并储存在了branch
中,这意味着leaf
中的Node
现在有两个所有者:leaf
和branch
。可以通过branch.children
从branch
中获得leaf
,不过无法从leaf
到branch
。leaf
没有到branch
的引用且并不知道他们相互关联。我们希望leaf
知道branch
是其父节点。稍后我们会这么做。
增加从子到父的引用
为了使子节点知道其父节点,需要在
Node
结构体定义中增加一个parent
字段。问题是parent
的类型应该是什么。我们知道其不能包含Rc<T>
,因为这样leaf.parent
将会指向branch
而branch.children
会包含leaf
的指针,这会形成引用循环,会造成其strong_count
永远也不会为0
.现在换一种方式思考这个关系,父节点应该拥有其子节点:如果父节点被丢弃了,其子节点也应该被丢弃。然而子节点不应该拥有其父节点:如果丢弃子节点,其父节点应该依然存在。这正是弱引用的例子!
所以
parent
使用Weak<T>
类型而不是Rc<T>
,具体来说是RefCell<Weak<Node>>
。现在Node
结构体定义看起来像这样:
use std::rc::{Rc, Weak};
use std::cell::RefCell;
#[derive(Debug)]
struct Node {
value: i32,
parent: RefCell<Weak<Node>>,
children: RefCell<Vec<Rc<Node>>>,
}
- 这样,一个节点就能够引用其父节点,但不拥有其父节点。我们更新
main
来使用新定义以便leaf
节点可以通过branch
引用其父节点:
use std::rc::{Rc, Weak};
use std::cell::RefCell;
#[derive(Debug)]
struct Node {
value: i32,
parent: RefCell<Weak<Node>>,
children: RefCell<Vec<Rc<Node>>>,
}
fn main() {
let leaf = Rc::new(Node {
value: 3,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});
println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
let branch = Rc::new(Node {
value: 5,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);
println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
}
- 创建
leaf
节点类似于示例中如何创建leaf
节点的,除了parent
字段有所不同:leaf
开始时没有父节点,所以我们新建了一个空的Weak
引用实例。 - 此时,当尝试使用
upgrade
方法获取leaf
的父节点引用时,会得到一个None
值。如第一个println!
输出所示:
leaf parent = None
- 当创建
branch
节点时,其也会新建一个Weak<Node>
引用,因为branch
并没有父节点。leaf
仍然作为branch
的一个子节点。一旦在branch
中有了Node
实例,就可以修改leaf
使其拥有指向父节点的Weak<Node>
引用。这里使用了leaf
中parent
字段里的RefCell<Weak<Node>>
的borrow_mut
方法,接着使用了Rc::downgrade
函数来从branch
中的Rc<Node>
值创建了一个指向branch
的Weak<Node>
引用。 - 当再次打印出
leaf
的父节点时,这一次将会得到存放了branch
的Some
值:现在leaf
可以访问其父节点了!当打印出leaf
时,我们也避免了如示例中最终会导致栈溢出的循环:Weak<Node>
引用被打印为 (Weak
):
leaf parent = Some(Node { value: 5, parent: RefCell { value: (Weak) },
children: RefCell { value: [Node { value: 3, parent: RefCell { value: (Weak) },
children: RefCell { value: [] } }] } })
- 没有无限的输出表明这段代码并没有造成引用循环。这一点也可以从观察
Rc::strong_count
和Rc::weak_count
调用的结果看出。
可视化strong_count和weak_count的改变
- 让我们通过创建了一个新的内部作用域并将
branch
的创建放入其中,来观察Rc<Node>
实例的strong_count
和weak_count
值的变化。这会展示当branch
创建和离开作用域被丢弃时会发生什么。
use std::rc::{Rc, Weak};
use std::cell::RefCell;
#[derive(Debug)]
struct Node {
value: i32,
parent: RefCell<Weak<Node>>,
children: RefCell<Vec<Rc<Node>>>,
}
fn main() {
let leaf = Rc::new(Node {
value: 3,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});
println!(
"leaf strong = {}, weak = {}",
Rc::strong_count(&leaf),
Rc::weak_count(&leaf),
);
{
let branch = Rc::new(Node {
value: 5,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);
println!(
"branch strong = {}, weak = {}",
Rc::strong_count(&branch),
Rc::weak_count(&branch),
);
println!(
"leaf strong = {}, weak = {}",
Rc::strong_count(&leaf),
Rc::weak_count(&leaf),
);
}
println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
println!(
"leaf strong = {}, weak = {}",
Rc::strong_count(&leaf),
Rc::weak_count(&leaf),
);
}
- 一旦创建了
leaf
,其Rc<Node>
的强引用计数为1
,弱引用计数为0
。在内部作用域中创建了branch
并与leaf
相关联,此时branch
中Rc<Node>
的强引用计数为1
,弱引用计数为1
(因为leaf.parent
通过Weak<Node>
指向branch
)。这里leaf
的强引用计数为2
,因为现在branch
的branch.children
中储存了leaf
的Rc<Node>
的拷贝,不过弱引用计数仍然为0
。
当内部作用域结束时,branch
离开作用域,Rc<Node>
的强引用计数减少为 0
,所以其 Node
被丢弃。来自 leaf.parent
的弱引用计数 1
与 Node
是否被丢弃无关,所以并没有产生任何内存泄漏!
如果在内部作用域结束后尝试访问 leaf
的父节点,会再次得到 None
。在程序的结尾,leaf
中 Rc<Node>
的强引用计数为 1
,弱引用计数为 0
,因为现在 leaf
又是 Rc<Node>
唯一的引用了。
所有这些管理计数和值的逻辑都内建于 Rc<T>
和 Weak<T>
以及它们的 Drop trait
实现中。通过在 Node
定义中指定从子节点到父节点的关系为一个Weak<T>
引用,就能够拥有父节点和子节点之间的双向引用而不会造成引用循环和内存泄漏。
- 这一章涵盖了如何使用智能指针来做出不同于 Rust 常规引用默认所提供的保证与取舍。
Box<T>
有一个已知的大小并指向分配在堆上的数据。Rc<T>
记录了堆上数据的引用数量以便可以拥有多个所有者。RefCell<T>
和其内部可变性提供了一个可以用于当需要不可变类型但是需要改变其内部值能力的类型,并在运行时而不是编译时检查借用规则。 - 因为
RefCell<T>
是只能在单线程上使用的。而且RefCell<T>
并不会在堆上分配内存,它仅用于基于数据段的静态内存 分配。
我们还介绍了提供了很多智能指针功能的 trait Deref
和 Drop
。同时探索了会造成内存泄漏的引用循环,以及如何使用 Weak<T>
来避免它们。