参考:https://kaisery.github.io/trpl-zh-cn/ch15-00-smart-pointers.html
指针 (pointer )是一个包含内存地址的变量的通用概念。这个地址引用,或 “指向”(points at)一些其他数据。
- 引用 (reference ):Rust 中最常见的指针是第四章介绍的 。引用以
&
符号为标志并借用了他们所指向的值。除了引用数据没有任何其他特殊功能。它们也没有任何额外开销,所以应用得最多。 - 智能指针 (smart pointers )是一类数据结构,他们的表现类似指针,但是也拥有额外的元数据和功能。
- 智能指针的概念并不为 Rust 所独有;其起源于 C++ 并存在于其他语言中。
- 通常使用结构体实现。区别于常规结构体的显著特性在于:智能指针实现了
Deref
和Drop
trait。 - 是一个在 Rust 经常被使用的通用设计模式。很多库都有自己的智能指针,你也可以编写属于你自己的智能指针。
普通引用和智能指针的区别:
- 数据所有权:
- reference:没有所有权转移的概念,因为只是引用/借用数据
- smart pointer:大部分情况下, 拥有 他们指向的数据,也就是所有权可以转移
- 特殊功能:
- reference:无
- smart pointer:
- 拥有额外的元数据和功能。
String
、Vec<T>
属于智能指针,它们也带有元数据(比如他们的容量)和额外的功能或保证(String
的数据总是有效的 UTF-8 编码)。 Deref
trait 允许智能指针结构体实例表现的像引用一样,这样就可以编写既用于引用、又用于智能指针的代码。Drop
trait 允许我们自定义当智能指针离开作用域时运行的代码。
- 拥有额外的元数据和功能。
- reference:无
考虑到智能指针这里将会讲到的是来自标准库中最常用的一些:
Box<T>
,用于在堆上分配值Rc<T>
,一个引用计数类型,其数据可以有多个所有者。引用计数智能指针记录总共有多少个所有者,并当没有任何所有者时负责清理数据。Ref<T>
和RefMut<T>
,通过RefCell<T>
访问。(RefCell<T>
是一个在运行时而不是在编译时执行借用规则的类型)。
另外我们会涉及 内部可变性 (interior mutability )模式,这是不可变类型暴露出改变其内部值的 API。我们也会讨论 引用循环 (reference cycles )会如何泄漏内存,以及如何避免。
Box<T>
指向堆上的数据
Box<T>
:
- 最简单直接的智能指针,其类型是
Box<T>
- 数据被储存在堆上:允许你将一个值放在堆上而不是栈上,留在栈上的则是指向堆数据的指针。
- box 没有性能损失,不过也没有很多额外的功能
多用于如下场景:
- 当有一个在编译时未知大小的类型,而又想要在需要确切大小的上下文中使用这个类型值的时候:比如创建递归类型
- 当有大量数据并希望在确保数据不被拷贝的情况下转移所有权的时候:
转移大量数据的所有权可能会花费很长的时间,因为数据在栈上进行了拷贝。为了改善这种情况下的性能,可以通过 box 将这些数据储存在堆上。接着,只有少量的指针数据在栈上被拷贝。 - trait 对象 (trait object ):当希望拥有一个值并只关心它的类型是否实现了特定 trait 而不是其具体类型的时候
这里定义了变量fn main() {
let b = Box::new(5);
println!("b = {}", b);
}
b
,其值是一个指向被分配在堆上的值5
的Box
。这个程序会打印出b = 5
;在这个例子中,我们可以像数据是储存在栈上的那样访问 box 中的数据。正如任何拥有数据所有权的值那样,当像b
这样的 box 在main
的末尾离开作用域时,它将被释放。这个释放过程作用于 box 本身(位于栈上)和它所指向的数据(位于堆上)。
将一个单独的值存放在堆上并不是很有意义,因此并不常见。将像单个i32
这样的值储存在栈上,也就是其默认存放的地方在大部分使用场景中更为合适。
对于正常情况下的非递归类型,Rust 如何决定需要多少空间来存放呢?以枚举体为例:
当 Rust 需要知道要为enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
Message
值分配多少空间时,它可以检查每一个成员并发现Message::Quit
并不需要任何空间(因为是 unit 类型),Message::Move
需要足够储存两个i32
值的空间,依此类推。因此,Message
值所需的空间等于储存其最大成员的空间大小。
递归类型 (recursive type ):
其值的一部分可以是相同类型的另一个值。
- 这种值的嵌套,理论上可以无限的进行下去,所以 Rust 不知道递归类型需要多少空间。(Rust 需要在编译时知道类型占用多少空间)
- 可以利用
Box
有一个已知的大小指针特点,通过在循环类型定义中插入Box
类型,就可以创建递归类型了。 - cons list 就是一种递归类型 (cons = construct,不是 constant),它是一个来源于 Lisp 编程语言及其方言的数据结构。
- 在 Lisp 中,
cons
函数(“construct function” 的缩写)利用两个参数来构造一个新的列表,他们通常是一个单独的值和另一个列表。
cons 函数的概念涉及到更常见的函数式编程术语;“将x与y连接” 通常意味着构建一个新的容器而将x的元素放在新容器的开头,其后则是容器y的元素。 - cons list 的每一项都包含两个元素:当前项的值和下一项。其最后一项值包含一个叫做
Nil
的值且没有下一项。cons list 通过递归调用cons
函数产生。代表递归的终止条件(base case)的规范名称是Nil
,它宣布列表的终止。这里的Nil
不是“null” 或 “nil” 的概念,后两者代表无效或缺失的值,在 Rust 里用Option
类型解决了。 - 虽然函数式编程语言经常使用 cons list,但是它并不是一个 Rust 中常见的类型。大部分在 Rust 中需要列表的时候,
Vec<T>
是一个更好的选择。 - 从 cons list 作为开始,我们可以探索 Rust 如何使用
Box
毫不费力的定义一个递归数据类型。
- 在 Lisp 中,
不使用 Box
创建递归类型:
// src/main.rs
enum List {
Cons(i32, List), // 类型嵌套
Nil,
}
use crate::List::{Cons, Nil};
fn main() {
let list = Cons(1, Cons(2, Cons(3, Nil))); // 递归创建
}
遇到编译器报错:
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
值到底需要多少空间:编译器尝试计算出储存一个 List
枚举需要多少内存,并开始检查 Cons
成员,那么 Cons
需要的空间等于 i32
的大小加上 List
的大小。为了计算 List
需要多少内存,它检查其成员,从 Cons
成员开始。Cons
成员储存了一个 i32
值和一个List
值,这样的计算将无限进行下去。这个情况就如同下面的图描述那样:
使用 Box
创建递归类型:
在上面编译器给的提示中,“indirection” 意味着不同于直接储存一个值,我们将 间接 的储存一个指向值的指针。
#![allow(unused)]
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)))))); }
因为Box<T>
是一个指针,我们总是知道它需要多少空间:指针的大小并不会根据其指向的数据量而改变。
这意味着可以将Box
放入Cons
成员中而不是直接存放另一个List
值。Box
会指向另一个位于堆上的List
值,而不是存放在Cons
成员中。从概念上讲,我们仍然有一个通过在其中 “存放” 其他列表创建的列表,不过现在实现这个概念的方式更像是一个项挨着另一项,而不是一项包含另一项。Cons
成员将会需要一个 i32
的大小加上储存 Box
指针数据的空间。Nil
成员不储存值,所以它比 Cons
成员需要更少的空间。因此 List
值最多需要一个 i32
加上 Box
指针数据的大小。通过使用 Box
,打破了这无限递归的连锁,这样编译器就能够计算出储存 List
值需要的大小了。
现在 Cons
成员看起来只有一层,因为最外层的 Box
在栈上存放指针,指针指向存在堆上大小未知的剩余数据:Box
只提供了间接存储和堆分配,并没有任何其他特殊的功能,相比于其他智能指针,没有它们特殊功能带来的性能损失,所以他们可以用于像 cons list 这样间接存储是唯一所需功能的场景。#todo: 第十七章 box 的更多应用场景#
如前所述,Box<T>
类型是一个智能指针,因为它实现了 Deref
trait——它允许 Box<T>
值被当作引用对待。当 Box<T>
值离开作用域时,由于 Box<T>
类型 Drop
trait 的实现,Box<T>
所指向的堆数据也会被清除。让我们更详细的探索一下这两个 trait。
Deref
trait
Deref
trait 的作用:重载 解引用运算符 (dereference operator )*
,从而使自定义类型具有解引用功能,即把 智能指针当作常规引用处理。
因为 *
默认只作用于 引用类型,有了 Deref
trait 之后,便可对其他类型使用这个符号,最终采用 *引用类型/实现了Deref类型 变量名
语法来 访问引用指向的数据。
没有 Deref
trait 的话,编译器只会解引用 &
引用类型,无法对非 &
引用类型的数据结构使用 *
来解引用。
而解引用运算符 *
的功能:追踪指针的值,使用解引用相当于使用值。(注意这里不是乘法运算符或通配符)
fn main() {
let x = 5; // i32
let y = &x; // &i32
assert_eq!(5, x);
// 如果使用 y 就会出错
// can't compare `{integer}` with `&{integer}`
// 数字的引用与数字,因为它们是不同的类型
// 所以必须使用解引用运算符追踪引用所指向的值。
assert_eq!(5, *y); // 相当于 *(y.deref())
// z 是指向 x 值的 box 实例,而不是指向 x 值的引用
// Box 实现了 Deref trait
let z = Box::new(x);
// Box<T> 作为一种智能指针
// 可以类似于引用那样进行解引用
assert_eq!(5, *z); // 相当于 *(z.deref())
}
我们可以用 Deref
trait 粗略地定义类似 Box
的智能指针:
// src/main.rs
use std::ops::Deref;
// 从根本上说,Box<T> 被定义为包含一个元素的泛型元组结构体
// 所以定义一个 generics tuple struct 来存放任何类型的值
struct MyBox<T>(T);
// 定义实例化方法
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> { MyBox(x) }
}
impl<T> Deref for MyBox<T> {
// Deref trait 需要关联类型,即解引用成哪种类型
// 目前只需要返回 MyBox 内 field 的值,所以是同类型
type Target = T;
// 需要手动实现 deref 方法
// 这样 deref 就能返回 MyBox field 值的引用
// 从而通过 `*` 运算符访问的值
fn deref(&self) -> &T { &self.0 }
}
fn hello(name: &str) {
println!("Hello, {}!", name);
}
fn main() {
let m = MyBox::new(String::from("Rust")); // m 的类型: MyBox<String>
hello(&m);
// 等价于以下做法:
// hello(&(*m)[..]);
// hello(&(*m.deref())[..]);
// hello(&(*m.deref()).deref());
// 打印:Hello, Rust!
}
实现 Deref
trait 需要指明关联类型 Target
和手动实现 deref
方法。
你也许会好奇,Hello
函数需要 &str
类型的参数,&m
是 &MyBox<String>
类型,为什么可以直接传入 &m
而无需额外的处理?
这是 deref coercion (解引用强制多态) 技术做到的,即 所需要类型的引用 是“嵌套”在里面的类型的引用 时,“强制”让编译器“自动”引用到需要的类型,而无需手动写出中间 deref 方法 。即如果函数需要 &U
, &T<U>
被自动转换为所需的 &U
;如果函数需要的是 &T<U>
,那么就无需(自动)调用 deref
——总之,无论需要的是那种,都只需要简单传入 &T<U>
。
如果没有这个机制,你需要手动使用一次 deref: &(*(T<U>).deref())
,先用 *(T<U>).deref()
转换到 U 类型,然后再引用它。如果这个过程需要很多层类型转换,那么写出来的代码就很难理解,所以 deref coercion 让程序员避免了做这样的重复琐碎的工作。这个功能也使得我们可以编写更多同时作用于引用或智能指针的代码。
当所涉及到的类型定义了 Deref
trait,Rust 会分析这些类型并使用任意多次 Deref::deref
调用以获得匹配参数的类型。这些解析都发生在编译时,所以利用解引用强制多态并没有运行时惩罚!
当然能够自动一连串 deref 的前提是 在到达最终需要的类型之前,经过的类型都实现了 Deref
trait。
比如这里的 MyBox 结构体类型的成员被定义为泛型,其实例的成员是 String 类型 —— std 对 String 实现 Deref trait,且 其 deref 方法签名:pub fn deref(&self) -> &str
表明支持 deref 到 str 类型,所以经过两次自动调用 deref
方法,到达 hello
函数需要的 &str
。deref
方法为什么不直接返回值?如果直接返回值而不是值的引用,其值(的所有权)将被移出self
。在这里以及大部分使用解引用运算符的情况下,我们并不希望获取MyBox<T>
内部值的所有权。
所以直接把 deref 数据解引用赋给变量是不恰当的: let n: String = *(m.deref());
这个语句会被编译器拒绝,m.deref()
返回结果是不可变引用,对这个不可变引用进行解引用就会直接获取掉值的所有权,即它让变量 n 获取了变量 m 值的所有权,这违反了所有权规则。
但是可以像这个例子那样,*(m.deref())
放在获取引用的过程中,&(*m.deref()).deref()
最终会返回一个引用,而不是值。至于 &(*m)[..]
的含义,*m -> String 自动经过了一次 deref,String 使用 [..] 获得 slice,String的 slice 就是 str,最后加上 & 就变成了所需要的 &str。
可以拓展 deref coercion 到可变引用。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 无法假设将不可变引用转换为可变引用是可能的。
还有一个与 解引用多态 (deref coercion)相关、并且可以结合起来用的概念叫做 “自动引用和解引用”(automatic referencing and dereferencing ):当使用object.something()
调用方法时,Rust 会自动为object
添加&
、&mut
或*
以便使object
与方法签名匹配。注意,如果单独使用 object
,比如在赋值的时候 let a = object
,并不具备这种自动机制,因为 object 是 不可变引用、可变引用、值的时候,a 也是相应的类型,不会被改变。
Drop
trait
对于智能指针模式来说第二个重要的 trait 是 Drop
,其允许我们在值要离开作用域时执行一些代码。可以为任何类型提供 Drop
trait 的实现,同时所指定的代码被用于释放类似于文件或网络连接的资源。我们在智能指针上下文中讨论 Drop
是因为其功能几乎总是用于实现智能指针。例如,Box<T>
自定义了 Drop
用来释放 box 所指向的堆空间。
在其他一些语言中,我们不得不记住在每次使用完智能指针实例后调用清理内存或资源的代码。如果忘记的话,运行代码的系统可能会因为负荷过重而崩溃。在 Rust 中,可以指定每当值离开作用域时被执行的代码,编译器会自动插入这些代码。于是我们就不需要在程序中到处编写在实例结束时清理这些变量的代码 —— 而且还不会泄漏资源。
指定在值离开作用域时应该执行的代码的方式是实现 Drop
trait。Drop
trait 要求实现一个叫做 drop
的方法,它获取一个 self
的可变引用。
struct CustomSmartPointer {
data: String,
}
impl Drop for CustomSmartPointer { // Drop trait 包含在 prelude 中,所以无需导入它
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.");
// 当实例离开作用域 Rust 会自动调用 drop,并调用我们指定的代码
// 变量以被创建时相反的顺序被丢弃,所以 d 在 c 之前被丢弃
}
// 打印结果:
// CustomSmartPointers created.
// Dropping CustomSmartPointer with data `other stuff`!
// Dropping CustomSmartPointer with data `my stuff`!
我们并不能直截了当的禁用 drop
这个功能。通常也不需要禁用 drop
;整个 Drop
trait 存在的意义在于其是自动处理的。
然而,有时你可能需要提早清理某个值。一个例子是当使用智能指针管理锁时;你可能希望强制运行 drop
方法来释放锁以便作用域中的其他代码可以获取锁。Rust 并不允许我们主动调用 Drop
trait 的 drop
方法;当我们希望在作用域结束之前就强制释放变量的话,我们应该使用的是由标准库提供的 std::mem::drop
。
use std::mem::drop;
struct CustomSmartPointer {
data: String,
}
impl Drop for CustomSmartPointer { // Drop trait 包含在 prelude 中,所以无需导入它
fn drop(&mut self) {
println!("Dropping CustomSmartPointer with data `{}`!", self.data);
}
}
fn main() {
let c = CustomSmartPointer { data: String::from("some data") };
println!("CustomSmartPointer created.");
// c.drop(); /* 不能显式调用 drop */
drop(c);
println!("CustomSmartPointer dropped before the end of main.");
}
Rc
不可变数据多所有权
大部分情况下所有权是非常明确的:可以准确地知道哪个变量拥有某个值。然而,有些情况单个值可能会有多个所有者。例如,在图数据结构中,多个边可能指向相同的节点,而这个节点从概念上讲为所有指向它的边所拥有。节点直到没有任何边指向它之前都不应该被清理。
为了启用多所有权,Rust 有一个叫做 Rc<T>
的类型。其名称为 引用计数 (reference counting )的缩写。引用计数意味着记录一个值引用的数量来知晓这个值是否仍在被使用。如果某个值有零个引用,就代表没有任何有效引用并可以被清理。Rc<T>
用于当我们希望在堆上分配一些内存供程序的多个部分读取,而且无法在编译时确定程序的哪一部分会最后结束使用它的时候。如果确实知道哪部分是最后一个结束使用的话,就可以令其成为数据的所有者,正常的所有权规则就可以在编译时生效。
注意 Rc<T>
只能用于单线程场景;多线程程序中进行引用计数须使用 std::sync::Arc
。
继续 “cons list 例子” :两个列表, b
和 c
, 共享第三个列表 a
的所有权。
尝试使用 Box<T>
定义的 List
实现并不能工作。Cons
成员拥有其储存的数据,所以当创建 b
列表时,a
被移动进了 b
这样 b
就拥有了 a
。接着当再次尝试使用 a
创建 c
时,这不被允许,因为 a
的所有权已经被移动。
修改 List
的定义为使用 Rc<T>
代替 Box<T>
:
enum List {
Cons(i32, Rc<List>),
Nil,
}
use crate::List::{Cons, Nil};
use std::rc::Rc;
fn main() {
// `a` 中 `Rc<List>` 的初始引用计数 (strong count) 为1
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
println!("count after creating a = {}", Rc::strong_count(&a));
// 每次调用 `clone`,`a` 的 strong count 会增加1
let b = Cons(3, Rc::clone(&a));
println!("count after creating b = {}", Rc::strong_count(&a));
{
// // 每次调用 `clone`,`a` 的 strong count 会增加1
let c = Cons(4, Rc::clone(&a));
println!("count after creating c = {}", Rc::strong_count(&a));
} // 当 `c` 离开作用域时,`Drop` trait 的实现当 `Rc<T>` 值离开作用域时自动减少引用计数,所以 `a` 的 strong count 减1
println!("count after c goes out of scope = {}", Rc::strong_count(&a));
}
// 从这个例子我们所不能看到的是,在 `main` 的结尾,当 `b` 然后是 `a` 离开作用域时,此处计数会是 0,同时 `Rc<List>` 被完全清理
// 使用 `Rc<T>` 允许一个值有多个所有者,引用计数则确保只要任何所有者依然存在其值也保持有效
// 打印结果:
count after creating a = 1
count after creating b = 2
count after creating c = 3
count after c goes out of scope = 2
现在每一个 Cons
变量都包含一个值和一个指向 List
的 Rc<T>
。当创建 b
时,不同于获取 a
的所有权,这里会克隆 a
所包含的 Rc<List>
,这会将引用计数从 1 增加到 2 并允许 a
和 b
共享 Rc<List>
中数据的所有权。创建 c
时也会克隆 a
,这会将引用计数从 2 增加为 3。每次调用 Rc::clone
,Rc<List>
中数据的引用计数都会增加,直到有零个引用之前其数据都不会被清理。
关于 Rc::clone
:
Rc::clone
的实现并不像大部分类型的clone
实现那样对所有数据进行深拷贝。Rc::clone
只会增加引用计数,这并不会花费多少时间。深拷贝可能会花费很长时间。- 通过使用
Rc::clone
进行引用计数,可以明显的区别深拷贝类的克隆和增加引用计数类的克隆。当查找代码中的性能问题时,只需考虑深拷贝类的克隆而无需考虑Rc::clone
调用。 - 也可以调用
a.clone()
而不是Rc::clone(&a)
,不过在这里 Rust 的习惯是使用Rc::clone
。
在程序中每个引用计数变化的点,会打印出引用计数,其值可以通过调用 Rc::strong_count
函数获得。这个函数叫做 strong_count
而不是 count
是因为 Rc<T>
也有 weak_count
;在 “避免引用循环:将 Rc<T>
变为 Weak<T>
” 部分会讲解 weak_count
的用途。
通过不可变引用, Rc<T>
允许在程序的多个部分之间只读地共享数据。如果 Rc<T>
也允许多个可变引用,则会违反第四章讨论的借用规则之一:相同位置的多个可变借用可能造成数据竞争和不一致。不过可以修改数据是非常有用的!在下一部分,我们将讨论内部可变性模式和 RefCell<T>
类型,它可以与 Rc<T>
结合使用来处理不可变性的限制。
RefCell
内部可变
内部可变性 (Interior mutability ):即 在不可变值 内部 改变值。
- 是 Rust 中的一个设计模式,它允许你即使在有不可变引用时也可以改变数据,这通常是 “借用规则” 所不允许的。(借用规则第一条:不能在拥有不可变引用的同时拥有可变引用。)
- 这里的“内部”指 数据类型的方法(method),而不是类型之外的代码。比如对于变量(数据) a,
a.f()
的 f 就是 a 的内部方法,而f(a)
的 f 就是 a 的外部函数。 - 为了改变数据,该模式在数据结构中使用
unsafe
代码来模糊 Rust 通常的可变性和借用规则。 - 当可以确保代码在运行时会遵守 “借用规则”,即使编译器不能保证的情况,可以选择使用那些运用内部可变性模式的类型。
- 所涉及的
unsafe
代码将被封装进安全的 API 中,而外部类型仍然是不可变的。
引用 | Box
| Rc
| RefCell
的在 “借用规则” 上的区别:都必须满足 “借用规则”,只是规则检查/生效的阶段不同
- 对于 引用、
Box<T>
、Rc
,”借用规则” 的不可变性作用于 编译时。如果违反这些 “借用规则”,会得到一个编译错误。 - 对于
RefCell<T>
,这些不可变性作用于 运行时 。如果违反这些 “借用规则” ,编译成功的程序会 panic 并退出。
在编译时检查借用规则的优势是这些错误将在开发过程的早期被捕获,同时对运行时没有性能影响,因为所有的分析都提前完成了。为此,在编译时检查借用规则是大部分情况的最佳选择,这也正是其为何是 Rust 的默认行为。
相反在运行时检查借用规则的好处则是允许出现特定内存安全的场景,而它们在编译时检查中是不允许的。静态分析,正如 Rust 编译器,是天生保守的。但代码的一些属性不可能通过分析代码发现:其中最著名的就是 停机问题(Halting Problem) 。因为一些分析是不可能的,如果 Rust 编译器不能通过所有权规则编译,它可能会拒绝一个正确的程序;从这种角度考虑它是保守的。如果 Rust 接受不正确的程序,那么用户也就不会相信 Rust 所做的保证了。然而,如果 Rust 拒绝正确的程序,虽然会给程序员带来不便,但不会带来灾难。RefCell<T>
正是用于当你确信代码遵守借用规则,而编译器不能理解和确定的时候。
特定情况下,令一个值在其方法内部能够修改自身,而在其他代码中仍视为不可变,是很有用的。值方法外部的代码就不能修改其值了。Refcell
:
- 是一个获得内部可变性的方法。即不需要在外部的可变引用符号
&mut
,RefCell <T>
就能通过.borrow_mut()
得到T
的可变引用;也可以通过.borrow()
得到T
的不可变引用。 - 它并没有完全绕开借用规则,编译器中的借用检查器允许内部可变性并相应地在运行时检查借用规则。如果违反了这些规则,会出现 panic 而不是编译错误。
测试替身 (test double )是一个通用编程概念,它代表一个在测试中替代某个类型的类型。mock 对象 是特定类型的测试替身,它们记录测试过程中发生了什么以便可以断言操作是正确的。
如下是一个我们想要测试的场景:我们在编写一个记录某个值与最大值的差距的 lib crate,并根据当前值与最大值的差距来发送消息。该 lib 只提供记录与最大值的差距,以及何种情况发送什么消息的功能。
// src/lib.rs
pub trait Messenger {
fn send(&self, msg: &str); // 注意这里是 &self,是不可变的
}
pub struct LimitTracker<'a, T: Messenger> { // 生命周期参数 和 泛型+trait bound
messenger: &'a T, // 因为 field 是引用类型,所以显式指明生命周期
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) { // 注意这里是 &mut self,是可变的,因为需要修改 value
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 { // 这一部分是发送消息,messenger 具有 Messenger trait,实现了 send 方法
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!");
}
}
}
测试代码:目的是测试 set_value 方法改变 value 的值时,能否正常调用 send 方法来发送消息
#[cfg(test)]
mod tests {
use super::*;
struct MockMessenger { // 定义 mock 测试替身
sent_messages: Vec<String>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger { sent_messages: vec![] }
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
// 按照 Messenger trait 的 send 签名,&self 说明 MockMessenger 实例中的
// sent_messages 字段不能被改变值,然而实现此 trait 时,方法需要改变这个值
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);
}
}
测试并不通过,结果显示:
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 方法时,实际上改变了 messenger 字段的值,与源代码的 send 签名的不可变引用相违背。
如果要让测试通过,有两种方法:
按照编译器的提示,把源代码和测试中的 send 签名换成
&mut self
,然后对源代码的 LimitTracker 结构体 messenger 字段 和 测试中的 MockMessenger 实例都加上相应的mut
,使它们全部变成可变引用,才能符合 Rust 的引用规则。正如下面的代码所示:// src/lib.rs
pub trait Messenger {
fn send(&mut self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a mut T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where T: Messenger
{
pub fn new(messenger: &mut 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::*;
struct MockMessenger {
sent_messages: Vec<String>,
}
impl MockMessenger {
fn new() -> MockMessenger { MockMessenger { sent_messages: vec![] } }
}
impl Messenger for MockMessenger {
fn send(&mut self, message: &str) { self.sent_messages.push(String::from(message)); }
}
#[test]
fn it_sends_an_over_75_percent_warning_message() {
let mut mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mut mock_messenger, 100);
limit_tracker.set_value(80);
assert_eq!(mock_messenger.sent_messages.len(), 1);
}
}
如果出于某些原因,我们不修改源代码,只修改测试代码,而且对测试中的 send 方法也尽可能保留,我们还能在原本为 不可变引用的情况下,途中修改值吗?
使用 RefCell
对 MockMessenger.sent_messages 的类型进行修改,让程序在运行时对 sent_messages 执行可变借用检查,从而可以在即便 RefCell<T>
自身是不可变引用的情况下修改其内部的值。
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell; // 把 RefCell 类型引入作用域
struct MockMessenger {
sent_messages: RefCell<Vec<String>>, // 修改成 RefCell 类型
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: RefCell::new(vec![]),
}
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) { // 函数签名与源代码定义的一致,无需改动
// borrow_mut 方法把 RefCell 类型内部的数据以可变引用的方式取出
self.sent_messages.borrow_mut().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);
// RefCell 类型的 borrow 方法把 RefCell 内部的数据以不可变引用的方式取出
assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
}
}
注意带有注释的那部分代码,可以感受到 RefCell
内部可变性使用的关键:它既可以把内部的数据(这里的 Vec<String>
类型数据)以 borrow_mut 可变的方式使用,也可以 borrow 不可变的方式使用;而 RefCell<Vec<String>>
类型作为整体,我们没有违反外层 RefCell
在 send 方法上的 &self
不可变引用。
选择 Box<T>
,Rc<T>
或 RefCell<T>
的理由:
Rc<T>
允许相同数据有多个所有者;Box<T>
和RefCell<T>
有单一所有者。Box<T>
允许在编译时执行不可变或可变借用检查;Rc<T>
仅允许在编译时执行不可变借用检查;RefCell<T>
允许在运行时执行不可变或可变借用检查。- 因为
RefCell<T>
允许在运行时执行可变借用检查,所以我们可以在即便RefCell<T>
自身是不可变的情况下修改其内部的值。
此外,RefCell<T>
的一个常见用法是与 Rc
结合。如果有一个储存了 RefCell 的 Rc 的话,就可以得到有多个所有者 并且 可以修改的值了!
#[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));
// 1. 这里对指针类型使用了`自动解引用`功能来解引用 Rc<T> 以获取其内部的 RefCell<T> 值
// 2. borrow_mut 方法返回 RefMut<T> 智能指针,可以对其使用解引用运算符并修改其内部值
*value.borrow_mut() += 10;
println!("a after = {:?}", a);
println!("b after = {:?}", b);
println!("c after = {:?}", c);
}
// 打印结果
// 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>
,其提供线程间安全的内部可变性。https://doc.rust-lang.org/std/cell/index.html
引用循环与内存泄漏
参考:https://kaisery.github.io/trpl-zh-cn/ch15-06-reference-cycles.html
use std::cell::RefCell;
use std::rc::{Rc, Weak};
#[derive(Debug)]
struct Node {
value: i32,
parent: RefCell<Weak<Node>>,
children: RefCell<Vec<Rc<Node>>>,
}
fn main() {
// 实例化一个无 父/子节点的 Node:leaf
let leaf = Rc::new(Node { value: 3,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]), });
{
// 实例化带 子节点 的 Node:branch
let branch = Rc::new(Node { value: 5,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![Rc::clone(&leaf)]), });
// leaf 手动关联父节点 Node:branch
// leaf.parent 通过 RefCell.borrow_mut 获取到 Weak<Node> 的可变引用,用 `*` 解引用成 Weak<Node>
// Rc::downgrade 就是把 strong Rc pointer 转化成 weak Rc pointer,即这里的 Weak<Node>
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);
}
}
在内部作用域中创建了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
是否被丢弃无关,所以并没有产生任何内存泄漏!
关于 std::rc
模块中 struct Rc
和 Weak
的使用总结:
Rc
是智能指针,既可以作为普通的引用,使用Deref
和自动解引用,又可以使用创建多个不可变的所有权:Rc::clone(&a)
就是把a
的不可变指针复制一份,同时具有所有权、把 strong Rc pointer count +1- 当 strong count 为 0 的时候,把值清除
- 如果 drop 某些 clone 的变量之后,strong count 不为 0,则值不被清除
Weak
也是智能指针,和Rc
可以相互转化,但其特殊之处在于:- 创建的指针不具备所有权,通常使用
Rc::downgrade
,把Rc
转换成Weak
类型 - 指向的对象实际可能不存在(比如被 drop 或者离开了作用域),所以使用
.update
把Weak
转换成Rc
类型时,是Option<Rc>
Weak::new()
没有给数据分配内存,强弱引用计数都是 0,调用.update
得到的是Option<Rc>
的None
值Weak
可以避免Rc
指针的引用循环,比如树(tree)的节点(node)中,父子节点相互指向的时候
- 创建的指针不具备所有权,通常使用
Rc
和Weak
都可以有 强弱引用计数:a: Rc<..>
,b: Weak<..>
- 强引用计数:
Rc::strong_count(&a)
、Weak::strong_count(&b)
- 弱引用计数:
Rc::weak_count(&a)
、Weak::weak_count(&b)
- 强引用计数:
对上面例子代码更详细的注解和可视化:
use std::cell::RefCell;
use std::rc::{Rc, Weak};
#[derive(Debug)]
pub struct Node {
value: i32,
parent: RefCell<Weak<Node>>,
children: RefCell<Vec<Rc<Node>>>,
}
impl Node {
pub fn print_node_body(&self, desc: &str, prefix: &str) {
println!("{}{} = {:#?} ", prefix, desc, self);
}
pub fn print_parent_node_body(&self, prefix: &str) {
// `upgrade` 让 Weak pointer 转化成 Strong pointer 的 Option 类型:Option<Rc<T>>
if let Some(x) = self.parent.borrow().upgrade() {
println!("{}parent = {:#?}", prefix, x);
} else {
println!("{}parent = None", prefix);
}
}
pub fn print_ref_count(rc_node: &Rc<Node>, desc: &str, prefix: &str) {
println!("{}{} as <Rc> strong = {}, weak = {} ",
prefix,
desc,
Rc::strong_count(rc_node),
Rc::weak_count(rc_node));
}
pub fn print_weak_ref_count(rc_node: &Weak<Node>, desc: &str, prefix: &str) {
println!("{}{} as <Weak> strong = {}, weak = {} ",
prefix,
desc,
Weak::strong_count(rc_node),
Weak::weak_count(rc_node));
}
pub fn print_parent_weak_count(&self, desc: &str, prefix: &str) {
Node::print_weak_ref_count(&self.parent.borrow(), desc, prefix);
}
}
fn main() {
let leaf = Rc::new(Node { value: 3,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]), });
leaf.print_node_body("leaf", "[leaf is a single node without branch]\n");
// leaf 为 Rc<Node> 类型,即 strong pointer,leaf strong = 1
// leaf 没有以 Weak<Node> 类型被引用,leaf weak = 0
Node::print_ref_count(&leaf, "\nleaf", "");
// None,因为 leaf.parent 指向 Weak::new()
leaf.print_parent_node_body("▲ leaf.");
// leaf.parent 使用 Weak::new() 初始化,没有分配内存,没有指向实际的数据
// 所以弱引用计数是 0;其强指针类型 Rc 也没有被引用所以强引用计数也为 0
leaf.print_parent_weak_count("▲ leaf.parent", "");
{
// 在内部作用域中创建了 branch 关联 leaf
let branch = Rc::new(Node { value: 5,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![Rc::clone(&leaf)]), });
println!("\n------ inside scope begins: a branch added");
// branch 为 Rc<Node> 类型,即 strong pointer,branch strong = 1
// branch 自身没有以 Weak<Node> 类型被引用,branch weak = 0
Node::print_ref_count(&branch, "branch", "[when a branch links to leaf]\n");
// ★ leaf strong count = 2,原因:
// 1. 自身是 Rc<Node> 类型
// 2. branch.children 里 Rc::clone(&leaf) 作为 Rc<Node> 被引用,强引用计数再 +1
// ★ leaf weak count = 0,因为 leaf 没有作为 Weak<Node> 被 branch.children 引用
// 因为 branch 引用 leaf 的方式是 Rc<Node>,不是 Weak<Node>
Node::print_ref_count(&leaf, "leaf", "");
// branch 以 Rc 引用 leaf,未对其字段引用,所以 leaf.parent 强弱引用计数未变
leaf.print_parent_weak_count("▲ leaf.parent", "");
// 关键性的一步:用 Weak pointer 来避免循环引用
// leaf.parent 作为 Weak<Node> 类型,指向了 branch 的 Weak<Node> 类型
// 所以 branch 弱引用计数变成 1
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);
// branch 为 Rc<Node> 类型,即 strong pointer,branch strong = 1
// branch 被 leaf.parent 通过 Weak<Node> 引用,branch weak = 1
Node::print_ref_count(&branch,
"branch",
"\n[after the leaf upgraded the link to branch]\n");
// leaf 强弱引用未变:因为 leaf 没有以 Rc 或 Weak 类型被引用
Node::print_ref_count(&leaf, "leaf", "");
// leaf.parent 不再是 Weak::new(),而是 Weak<Node> 类型
// 且指向 branch Weak<Node> 所以 weak count = 1
// 这里通过 Weak 的 .upgrade 方法,成功以 strong pointer 指向了 branch
// 因此此时 branch 的强引用计数 +1,加上 branch 创建非 new 的 Rc,
// leaf.parent...upgrade 其实就是指向 branch Rc,所以两者强引用计数一样
// leaf.parent.borrow().upgrade() 创建了指向 branch 的 strong pointer
Node::print_ref_count(&leaf.parent.borrow().upgrade().unwrap(),
"leaf.parent as <Rc>",
"▲ (may be confusing) ");
// 但是,leaf.parent 自身是 RefCell<Weak<Node>> 类型,自动解引用之后为
// Weak 类型,只以 Weak 类型查看强弱引用计数,不转化成强指针
// strong = 1:branch 自身 Rc
// weak = 1:parent 自身 Weak
println!("▲ leaf.parent as <Weak> strong = {}, weak = {}",
Weak::strong_count(&leaf.parent.borrow()),
Weak::weak_count(&leaf.parent.borrow()));
let a_strong_pointer_to_branch = leaf.parent.borrow().upgrade().unwrap();
// let b_strong_pointer_to_branch = leaf.parent.borrow().upgrade().unwrap();
println!("\n*** testing *** [add a strong Rc pointer to branch]");
Node::print_ref_count(&a_strong_pointer_to_branch,
"a_strong_pointer_to_branch",
"*** testing *** ");
Node::print_ref_count(&branch, "branch", "*** testing *** ");
Node::print_ref_count(&leaf, "leaf", "*** testing *** ");
drop(a_strong_pointer_to_branch);
println!("*** testing *** [strong pointer dropped]");
Node::print_ref_count(&branch, "branch", "*** testing *** ");
Node::print_ref_count(&leaf, "leaf", "*** testing *** ");
// 以下打印 branch、leaf、leaf.parent 的 Node 内容:
branch.print_node_body("branch",
"\nshow the node body of branch, leaf, and leaf.parent:\n");
// leaf 的结构看起来和创建时一样,且没有出现循环引用
leaf.print_node_body("leaf", "\n(no reference cycles, and look alike before)\n");
// leaf.parent 自动解引用为 Weak<Node> 类型
// 手动调用 .borrow().upgrade() 方法获取数据不可变引用
// 根据关联关系:leaf.parent 就是 branch
leaf.print_parent_node_body("\n▲ leaf.");
println!("(leaf's parent is exactly the branch!)\n------ inside scope ends: the branch removed");
}
println!("");
// branch 离开作用域被 dropped,leaf 回到被创建的初始状态
leaf.print_node_body("leaf", "[back to a single leaf without branch]\n");
println!("▲ leaf.parent = {:#?}", leaf.parent.borrow().upgrade());
leaf.print_parent_weak_count("▲ leaf.parent", "");
}
运行结果:
[leaf is a single node without branch]
leaf = Node {
value: 3,
parent: RefCell {
value: (Weak),
},
children: RefCell {
value: [],
},
}
leaf as <Rc> strong = 1, weak = 0
▲ leaf.parent = None
▲ leaf.parent as <Weak> strong = 0, weak = 0
------ inside scope begins: a branch added
[when a branch links to leaf]
branch as <Rc> strong = 1, weak = 0
leaf as <Rc> strong = 2, weak = 0
▲ leaf.parent as <Weak> strong = 0, weak = 0
[after the leaf upgraded the link to branch]
branch as <Rc> strong = 1, weak = 1
leaf as <Rc> strong = 2, weak = 0
▲ (may be confusing) leaf.parent as <Rc> as <Rc> strong = 2, weak = 1
▲ leaf.parent as <Weak> strong = 1, weak = 1
*** testing *** [add a strong Rc pointer to branch]
*** testing *** a_strong_pointer_to_branch as <Rc> strong = 2, weak = 1
*** testing *** branch as <Rc> strong = 2, weak = 1
*** testing *** leaf as <Rc> strong = 2, weak = 0
*** testing *** [strong pointer dropped]
*** testing *** branch as <Rc> strong = 1, weak = 1
*** testing *** leaf as <Rc> strong = 2, weak = 0
show the node body of branch, leaf, and leaf.parent:
branch = Node {
value: 5,
parent: RefCell {
value: (Weak),
},
children: RefCell {
value: [
Node {
value: 3,
parent: RefCell {
value: (Weak),
},
children: RefCell {
value: [],
},
},
],
},
}
(no reference cycles, and look alike before)
leaf = Node {
value: 3,
parent: RefCell {
value: (Weak),
},
children: RefCell {
value: [],
},
}
▲ leaf.parent = Node {
value: 5,
parent: RefCell {
value: (Weak),
},
children: RefCell {
value: [
Node {
value: 3,
parent: RefCell {
value: (Weak),
},
children: RefCell {
value: [],
},
},
],
},
}
(leaf's parent is exactly the branch!)
------ inside scope ends: the branch removed
[back to a single leaf without branch]
leaf = Node {
value: 3,
parent: RefCell {
value: (Weak),
},
children: RefCell {
value: [],
},
}
▲ leaf.parent = None
▲ leaf.parent as <Weak> strong = 0, weak = 0
Rust 使用引用构造树形结构其实很复杂,在 GUI 领域的开发中,动态类型是基本且重要的,需要很多状态和交互。
我们可以换种思路来实现树,使用 Vec
和 Struct
:https://rust-leipzig.github.io/architecture/2016/12/20/idiomatic-trees-in-rust/