14.智能指针
指针(pointer)是一个通用概念,它指代那些包含内存地址的变量。这个地址被用于索引,被用于指向内存中的其他数据。引用是用&
符号表示,会借用它所指向的值。引用除了指向数据外没有任何其他功能,也没有任何额外的开销,它是Rust中最为常见的一种指针。
而智能指针则是一些数据结构,它们的行为类似于指针但拥有额外的元数据和附加功能。
引用和智能指针之间还有另外一个差别:引用是只借用数据的指针;而与之相反地,大多数智能指针本身就拥有它们指向的数据。
之前介绍过的String
和Vec<T>
就可以算作智能指针,因为它们都拥有一篇内存区域并允许用户对其进行操作,它们还拥有元数据并提供额外的功能或保障。
我们通常会使用结构体来实现智能指针,但区别于一般结构体的地方在于它们会实现Deref
和Drop
这两个trait。
- Deref trait:使得智能指针结构体的实例拥有与引用一致的行为,它使你可以编写出能够同时用与引用和智能指针的代码;
- Drop trait:则使你可以自定义智能指针离开作用域时运行的代码;
我们将讨论的重点集中到标准库中最为常见的那些智能指针上: - Box<T>:可以用于在堆上分配值;
- Rc<T>:允许多重所有权的引用技术类型;
- Ref<T>和RefMut<T>:可以通过RefCell<T>访问,是一种可以在运行时而不是编译时执行借用规则的类型;
内部可变性(interior mutability)模式,使用这一模式的不可变类型会暴露处改变自己内布值得API。
一、使用Box<T>在堆上分配数据
最为简单直接地一种智能指针Box
,它的类型被写作Box<T>
。Box使我们可以将数据存储在堆上,并在栈中保留一个指向堆数据的指针。当然,它们也无法提供太多的额外功能。
Box常常被用于如下场景:
- 当你拥有一个无法在编译时确定大小的类型,但又想要在一个要求固定尺寸的上下文环境中使用这个类型的值时;
- 当你需要传递大量数据的所有权,但又不希望产生大量数据的复制行为时;
- 当你希望拥有一个实现了执行trait的类型值,但又不关心具体的类型时;
1、使用Box<T>在堆上存储数据
Box<T>
语法,示例展示了如何使用Box在堆上存储一个i32值:
fn main() {
let b = Box::new(5);
println!(" b = {}", b);
}
和其他任何拥有所有权的值一样,Box会在离开自己的作用域时被释放。Box被释放的东西除了有存储在栈上的指针,还有它指向的那些数据。
2、使用Box定义递归类型
Rust必须在编译时知道每一种类型占据的空间大小,但有一种被称作递归的类型却无法在编译时被确定具体大小。递归类型的值可以在自身中存储另一个相同类型的值,因为这种嵌套在理论上可以无穷尽地进行下去,所以Rust根本无法计算出一个递归类型需要地具体空间。但是,Box有一个固定地大小,我们只需要在递归类型地定义中使用Box便可以创建递归类型。
1.有关链接列表的更多信息
链接列表(cons list)是一种来自Lisp的编程语言与其方言的数据结构。在Lisp中,cons函数会将两个参数组成一个二元组,而这个元组通常由一个值与另一个二元组组成。通过这种不断嵌套元组的形式可以最终组成一个列表。
链接列表的每一项都包含了两个元素:当前项的值及下一项。列表中的最后一项时一个被称作Ni1且不包含下一项的特殊值。我们通过反复调用cons函数来生成链接列表,并使用规范名称Ni1来作为列表的最终标记。
如下示例尝试使用枚举定义一个链接列表。这段代码暂时无法通过编译,因为我们不能确定List类型的具体大小。
enum List {
Cons(i32, List);
Nil,
}
如下实例演示了使用List类型来存储列表1,2,3的方法:
enum List {
Cons(i32, List),
Nil,
}
use crate::List::{Cons, Nil};
fn main() {
let list = Cons(1, Cons(2, Cons(3, Nil)));
}
第一个Cons变体包含了1和另外一个List值。这个List值作为另外一个Cons变体包含了2和另外一个List值。这个List依然是一个Cons变体,它包含了3与一个特殊的List值,也就是最终的非递归变体Ni1,它代表了列表的结束。
上面的错误提示信息指出这个类型“拥有无限大小”,这是因为我们在定义List时引入了一个递归的变体,它直接持有了另一个相同类型的值。这也意味着Rust无法计算出存储一个List值需要消耗多大的空间。
1.计算一个非递归类型的大小
回顾之前的实例,定义的Message枚举:
enum Message {
Quit,
Move { x: i32, y: i32},
Write(String),
ChangeColor(i32, i32, i32),
}
为了计算出Message值需要多大的存储空间,Rust会遍历枚举中的每一个成员来找到需要最大空间的那个变体。在Rust眼中,Message::Quit
不占用任何空间,Message::Move
需要两个存储i32值的空间,以此类推。因为每个时间点只会有一个变体存在,所以Message值需要的空间大小也就是能够存储得下最大变体的空间大小。
我们将这个模式代入Cons List。编译器会首先检查Cons变体,并发现它持有一个i32类型的值及另外一个List类型的值。因此,Cons变体需要的空间也就是等于一个i32值得大小加上一个List值得大小。为了确定List值所需得空间大小,编译器又会从Cons开始遍历其下所有变体,这样得检查过程就会无穷尽得运行下去。
2.使用Box<\T>给递归类型一个已知得大小
Rust无法计算出要为定义为递归的类型分配多少空间,所以编译器也给出了如下错误提示。这个错误提示也提供了有用的建议:
在建议中indirection
意味着,我们应该噶便数据结构来存储指向这个值的指针,而不是直接地存储这个值。
因为Box<T>
是一个指针,所以Rust总是可以确定Box<T>
地具体大小。指针的大小总是恒定的,它不会因为指向数据的大小而产生变化。这也意味着我们可以在Cons变体中存放一个Box<T>
而不是直接存放另一个List值。而Box<T>
则会指向下一个List并存储在堆上,而不是直接存放在Cons变体中。
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的空间和一部分存储装箱指针数据的空间。另外,由于Ni1变体没有存储任何值,所以它需要的空间比Cons变体小。现在,我们知道任意的List值都只需要占用一个i32值加上一个装箱指针的大小。通过使用装箱,我们打破了无限递归的过程,进而使编译器可以计算出存储一个List值需要多大的空间。如下时Cons变体的结构:
Box<T>
属于智能指针的一种,因为它实现了Deref trait,并允许我们将Box<T>的值当作引用来对待。当一个Box<T>值离开作用域时,因为它实现了Drop trait,所以Box<T>指向的堆数据会自动被清理释放掉。
二、通过Deref trait将智能指针视作常规引用
实现Deref trait使我们可以自定义解引用运算符*
的行为。通过实现Deref,我们可以将智能指针视作常规引用来进行处理。这也意味着,原本用于处理引用的代码可以不加修改地用于处理智能指针。
1、使用解引用运算符跳转到指针指向的值
常规引用就是一种类型的指针。如下示例我们创建了一个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);
修改为assert_eq!(5, y);
,则会触发编译错误:
由于数值和引用是两种不同的类型,所以你不能直接比较这两者。你必须使用解引用运算符来跳转到引用指向的值。
2、把Box<T>当成引用来操作
我们使用Box<T>
来代替上述示例的引用,此时的解引用运算符能够正常工作:
fn main() {
let x = 5;
let y = Box::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
这个示例与之前示例的唯一区别在于我们将y设置为了一个指向x值的Box指针,而不是一个指向x值得引用。在最后得断言中,我们依然可以使用解引用来跟踪装箱指针。
3、定义我们自己的智能指针
Box<T>
类型最终被定义为一个拥有单元素的元组结构体,如下示例以相同的方式定义了MyBox<T>
类型。初次之外,我们还定义了一个与Box<T>
的new函数作为类似的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实例作为结果。
让我们将MyBox
结构体代入之前的示例中,因为Rust还不知应该如何去解引用MyBox,所以当前示例无法通过编译:
//示例15-9
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
因为我们没有为MyBox<T>
类型实现解引用功能,所以这个解引用操作还无法生效。为了使用*
完成解引用操作,我们需要实现Deref trait
。
4、通过实现Deref trait来讲类型视作引用
为了实现某个trait,我们需要为该trait的方法执行具体的行为。而标注库中的Deref trait则要求我们实现一个deref方法,该方法会借用self并返回一个指向内部数据的引用。
如下示例MyBox实现了Deref:
use std::ops::Deref;
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
type Target = T;
:定义了Deref trait的一个关联类型。
我们在deref
方法体中填入&self.0
,这意味着deref会返回一个指向值得引用,进而允许调用者通过*
运算符访问值。
//示例15-10
use std::ops::Deref;
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
在没有Deref trait的情形下,编译器只能对&
形式的常规引用执行解引用操作。deref方法使编译器可以从任何实现了Deref的类型中获取值,并能够调用deref方法来获得一个可以进行解引用操作的引用。
我们在示例15-9中编写的*y
会被Rust隐式地展开为:
*(y.deref())
Rust使用*
运算符来代替deref方法和另外一个朴素的解引用操作,这样我们就不用考虑是否需要调用deref方法了。这一特性是我们可以用完全相同的方法编写代码来处理常规引用及实现了Deref trait的类型。
所有权系统决定了deref方法需要返回一个引用,而*(y.deref())
的最外层依然需要一个朴素的解引用操作。假设deref方法直接放回了值而不是指向值得引用,那么这个值就会被移出self。在大多数使用解引用运算符的场景下,我们并不希望获得MyBox<T>
内部值得所有权。
需要注意的是,这种将*
运算符替换为deref方法和另外一个朴素*
运算符的过程,对代码中的每个*
都只会进行一次。因为*
运算符的替换不会无穷尽地递归下入,所以我们才能在代码中得到i32类型的值。
5、函数和方法的隐式解引用转换
解引用转换(deref coercion)是Rust为函数和方法的参数提供的一种便捷特性。当某个特性T实现了Deref trait时,她能够将T的引用转换为T经过Deref操作后生成的引用。当我们将某个特定类型的值引用作为参数传递给函数或方法,但传入的类型与参数类型不一致时,解引用转换就会自动发生。编译器会插入一系列的deref方法调用来将我们提供的类型转换为参数所需的类型。
Rust通过实现解引用转换功能,使程序员在调用函数或方法时无须多次显式地使用&
和*
运算符来进行引用和解引用操作。
如下实例展示了一个接收字符串切片作为参数地函数定义:
//示例15-11
fn hello(name: &str) {
println!("Hello, {}", name);
}
借助解引用转换特性,我们既可以将字符串切片作为参数传入hello
函数,也可以将MyBox<String>
值得引用传入hello
函数:
//示例15-12
use std::ops::Deref;
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
fn hello(name: &str) {
println!("Hello, {}", name);
}
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&m);
}
我们在上面得代码中将参数&m
传入了hello函数,而&m
正是一个指向MyBox<String>
值得引用。因为我们在15-10中为MyBox<T>
实现了Deref trait,所以Rust可以通过调用deref来讲&MyBox<String>
转换为&String
。因为标准库为String提供得Deref实现会返回字符串切片,所以Rust可以继续调用deref来讲&String
转换为&str
,并最终与hello函数得定义相匹配。
如果Rust没有解引用转化功能,那么为了将&MyBox<String>
类型中得值传入hello函数,就不得不用下述示例代替15-12中的代码:
//示例15-12
use std::ops::Deref;
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
fn hello(name: &str) {
println!("Hello, {}", name);
}
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&(*m)[..]);
}
代码中的(*m)
手下将MyBox<String>
进行解引用得到String,然后通过&
和[..]
来获取包含整个String的字符串切片以便匹配hello函数的签名。缺少了解引用转换会使得代码难以阅读、编写和理解。
6、解引用转换与可变性
使用Deref trait
能够重载不可变引用的*
运算符。与之类似,使用DeferMut trait
能够重载可变引用的*
运算符/
Rust会在类型与trait满足下面3中情形时执行解引用转换:
- 当
T: Deref<Target=U>
时,允许&T
转换为&U
; - 当
T: DerefMut<Target=U>
时,允许&mut T
转换为&mut U
; - 当
T: Deref<Target=U>
时,允许&mut T
转换为&U
;
前两种情形除可变性之外是完全相同的。情形三有些微妙:Rust会将一个可变引用自动转换为一个不可变引用。但这个过程绝对不会逆转,也就是说不可变引用永远不能转换为可变引用。因为按照借用规则,如果存在一个可变引用,那么它就必须是唯一的引用。将一个可变引用转为不可变引用肯定不会破坏借用规则,但将一个不可变引用转换为可变引用则要求这个引用必须是唯一的,而借用规则无法保证这一点。因此,Rust无法将不可变引用转为可变引用视作一个合理的操作。
三、借助Drop trait在清理时运行代码
另一个对智能指针十分重要的trait就是Drop,它允许我们在变量离开作用域时执行某些自定义操作。在某些语言中,开发者必须在使用完智能指针后手动释放内存或资源。一旦它们忘记这件事,系统就可能会出现资源泄露并最终引发过载崩溃。在Rust中,我们可以为值指定离开作用域时需要执行的代码,而编译器则会自动将这些代码插入合适的地方。因此,你不用在程序中众多实例销毁处放置清理代码,也不会产生任何的资源泄露
我们通过实现Drop trait来指定离开作用域时需要运行的代码。Drop trait要求实现一个接收self可变引用作为参数的drop函数。
如下示例,定义了CustomSmartPointer
结构体,它唯一的功能是在离开作用域时打印一行文字:Dripping CustomSmartPointer!。通过这个示例,我们可以观察到Rust调用drop函数的时间:
//示例15-14
struct CustomSmartPointer {
data: String,
}
impl Drop for CustomSmartPointer {
fn drop(&mut self) {
println!("Drop CustomSmartPointer with data `{}`!", self.data);
}
}
fn main() {
let c = CustomSmartPointer { data: String::from("my stuff") };
let d = CustomSmartPointer { data: String::from("other stuff") };
println!("CustomSmartPointer created");
}
这段代码没有显式地将Drop trait引入作用域,因为它已经被包含在了预导入模块中。我们为CustomSmartPointer
结构体实现了Drop trait
,并在drop方法中调用了println!
,这些打印出来地文本可以用来展示Rust调用drip函数的时间。实际上,任何你想要在类型实例离开作用域时运作的逻辑多可以放在drop函数体内。
Rust在实例离开作用域时自动调用了我们编写的drop代码。因为变量的丢弃顺序与创建顺序相反,所以d在c之前被丢弃。在实际开发中,通常需要为指定类型执行清除逻辑而不是打印文本。
1、使用std::mem::drop提前丢弃值
遗憾的是,我们无法直接禁用自动drop功能。不过我们常常会碰带需要提前清理一个值的情形:一个例子是使用智能指针来管理锁时:你也许会希望强制运行drop方法来提前释放锁,从而允许同一作用域内的其他代码来获取它。Rust并不允许我们手动调用Drop trait的drop方法;但是我们可以调用标准库中的std::mem::drop
函数来提前清理某个值。
如果我们对示例15-14中的main函数进行了修改,以便手动调用Drop trait的drop方法,那么这段代码就会编译报错:
struct CustomSmartPointer {
data: String,
}
impl Drop for CustomSmartPointer {
fn drop(&mut self) {
println!("Drop CustomSmartPointer with data `{}`!", self.data);
}
}
fn main() {
let c = CustomSmartPointer { data: String::from("my stuff") };
let d = CustomSmartPointer { data: String::from("other stuff") };
c.drop();
println!("CustomSmartPointer created");
}
这条错误提示信息表明我们不能显示地调用drop。错误信息使用了术语析构函数(destructor),这个通用的编程概念被用来指代可以清理实例的函数,它与创建实例的构造函数(constructor)相对应。Rust中的drop函数就是这么一个析构函数。
因为Rust已经在main函数结尾的地方自动调用了drop,所以它不允许我们再次显式地调用drop。这种行为会试图对同一个值清理两次而导致重复释放(duble free)错误。
我们既不能在一个值离开作用域时禁止自动插入drop,也不能显示地调用drop方法。因此,如果必须要提前清理一个值,我们就需要使用std::mem::drop
函数。
std::mem::drop
函数不同于Drop trait中的drop
方法。我们需要手动调用这个函数,并将需要提前丢弃的值作为参数传入。因为该函数被放置在了预导入模块中,所以我们可以直接修改main函数来直接调用drop函数:
//示例15-16![[Pasted image 20230131094952.png]]
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.");
}
文本消息Dropping CustomSmartPointer with data
some data!
被打印在了CustomSmartPointer created.
和CustomSmartPointer dropped before the end of main.
之间,这说明drop方法被调用了并在此丢弃了c
。
四、基于引用技术的智能指针Rc<T>
所有权在大多数情况下都是清晰的:对于一个给定的值,你可以准确地判断出哪个变量拥有它。但在某些场景下,单个值也可能同时被多个所有者持有。因此Rust提供了一个名为Rc<T>
类型来支持多重所有权,它名称中地Rc是Reference counting的缩写。Rc<T>
类型的实例会在内部维护一个用于记录值引用次数的计数器,从而确定这个值是否仍在使用。如果对一个值的引用次数为零,那么就意味着这个值可以被安全地清理掉,而不会触发引用失效的问题。
需要主要的是,Rc<T>
只能被用于单线程场景中。
1、使用Rc<T>共享数据
我们曾在15-5的链接列表中使用了Box<T>
。这一次我们会创建两个列表,并让它们同时持有三个列表的所有权,结构如下:
首先创建一个包含5和10的列表a,并接着创建另外两个列表:以3来时的b和以4开始的c。b和c两个列表会连接至包含了5和10的列表a。换句话说,这两个列表将会共享第一个列表中的5和10。
基于Box<T>
实现的List无法实现这样的场景,如下示例无法正常运行:
//实例15-17
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));
}
Cons变体持有它存储的数据。因此,整个a列表会在我们创建b列表时被移动至b中。换句话说,b列表持有了a列表的所有权。当我们随后再次尝试使用a来创建c列表时就会出现编译错误,因为a已经被移走了。
我们当然可以改变Cons的定义来让它持有一个引用而不是所有权,并为其指定对应得胜名周期参数,但这个生命周期参数会要求列表中所有元素的存活时间都至少要和列表本身一样长。
另一种解决防范是,我们可以将List中的Box<T>
修改为Rc<T>
,如示例15-18所示。在这段新代码中,每个Cons变体都会持有一个值及一个指向List的Rc<T>
。我们只需要在创建b的过程中克隆a的Rc<List>
智能指针即可,而不需要获取a的所有权。这就会使a和b可以共享Rc<List>
数据的所有权,并使智能指针中的引用计数从1增加到2。随后,我们在创建c时也会同样克隆a并引用计数从2增加到3。每次调用Rc::clone
都会使引用计数增加,而Rc<List>
智能指针中的数据只有在引用计数器减少到0时才会被真正清理掉。
//实例15-18
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));
}
在这里你还可以调用a.clone()
而不是Rc::clone(&a)
来实现同样的效果,但Rust的惯例是在此场景下使用Rc::clone
,因为Rc::clone
不会执行数据的深度拷贝操作,这与绝大多数类型实现的clone方法明显不同。调用Rc::clone
只会增加引用计数,而这不会花费太多时间。但与此相对的是,深度拷贝则常常需要花费大量时间来搬运数据。
2、克隆Rc<T>会增加引用计数
我们继续修改示例15-18中的代码来观察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 a = {}", Rc::strong_count(&a));
{
let c = Cons(4, Rc::clone(&a));
println!("count after creating c = {}", Rc::strong_count(&a));
}
println!("count ager c goes out of scope = {}", Rc::strong_count(&a));
}
我们在每一个引用计数发生变化的地方调用Rc::strong_count
函数来读取引用计数并将它打印出来。这个函数之所以被命名为strong_count
(强引用计数)而不是count
(计数),是因为Rc<T>
类型还拥有一个weak_count
(弱引用计数)函数。
我们能够看到a存储的Rc<List>
拥有初始引用计数1,并在随后每次调用clone时增加1。而当c离开作用域被丢弃时,引用计数减少1。Rc<T>
的Drop实现会在Rc<T>
离开作用域时自动引用计数减1。
我们没有在这段输出中观察到b和a在main函数末尾离开作用域时的情形,但它们会让计数器的值减少到0并使Rc<List>
被彻底地清理掉。使用Rc<T>
可以使单个值拥有多个所有者,而引用计数机制则保证了这个值会在其拥有地所有者存活时一直有效,并在所有者全部离开作用域时被自动清理。
Rc<T>
通过不可变引用使你可以在程序的不同部分之间共享只读数据。
五、RefCell<T>和内部可变性模式
内部可变性(interior mutability)是Rust的的设计模式知以,它允许你在只持有不可变引用的前提下对数据进行修改:通常而言,类似的行为会被借用规则所禁止。为了能够改变数据,内部可变性模式在它的数据结构中使用了unsafe(不安全)代码来绕过Rust正常的可变性和借用规则。
1、使用RefCell<T>在运行时检查借用规则
与Rc<T>
不同,RefCell<T>
类型代表了其持有数据的唯一所有权。借用规则如下:
- 在任何给定的时间里,你只能拥有一个可变引用或者拥有任意数量的不可变引用。
- 引用总是有效的。
对于使用一般引用和Box<T>
的代码,Rust会在编译阶段强制代码遵守这些借用规则。而对于使用RefCell<T>
的代码,Rust则智慧在运行时检查这些代码,并在出现违法借用规则的情况下触发panic来提前中止程序。
与Rc<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>
自身是不可变的情况下修改其内部的值。
在不可变值内部改变值就是内部可变性模式。
2、内部可变性:可变地借用一个不可变的值
借用规则的一个推论是,你无法可变地借用一个不可变的值。例如,如下代码是无法通过编译的:
fn main() {
let x = 5;
let y = &mut x;
}
然而,在某些特定情况下,我们也会需要一个值在对外保持不可变性的同时能够在方法内部修改自身。除了这个值本身的方法,其余的代码则依然不能修改这个值,那么我们就可以使用RefCell<T>
。不过RefCell<T>
并没有完全绕开借用规则:我们虽然使用内部可变性通过了编译阶段的借用检查,但借用检查的工作仅仅是被延后到了运行阶段。如果你违反了借用规则,那么你就会得到一个panic!
。
1.内部可变性的应用场景:模拟对象
测试替代(test double)是一个通用的编程概念,它代表了那些在测试工作中被用作其他类型替代品的类型。而模拟对象(mock object)则指代了测试替代中某些特定的类型,它们会承担起记录测试过程的工作。
Rust没有和其他语言中类似的对象概念,也同样没有在标准库中提供模拟对象的测试功能。但是,我们可以自行定义一个结构体来实现与模拟对象相同的功能。
设计的测试场景如下:我们希望开发一个记录并对比当前值与最大值的库,它会基于当前值与最大值之间的接近程度向外传递信息。我们只会在这个库中记录当前值与最大值的接近程度,以及决定何时显示何种信息。使用库的应用程序需要自行实现发送消息的功能,例如在应用程序中打印信息、发送电子邮件、发送文字短信等。我们会提供一个Messengertrait供外部代码来实现这些功能,而使库本身不需要关心这些细节。示例如下:
//示例15-20:我们的库会记录当前值与最大值的接近程度并根据不同的程度输出警告信息
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: 'a + 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 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!");
}
}
}
这段代码的一个重点是Messenger trait
,它唯一的方法send可以接收self的不可变引用及一条文本消息作为此参数。我们创建的模拟对象就需要拥有这样的接口。另外一个重点则是LimitTracker
的sef_value
方法,我们需要对这个方法的行为测试。
实际上,我们需要在测试中确定的是,当某段程序使用一个实现了Messenger trait
的值与一个max
值来创建LimitTracker
实例时,传入的不同value值能够触发messenger发送不同的信息。
我们的模拟对象在调用send时只需要将受到的信息存档记录即可,而不需要真的去发送邮件或短信。使用模拟对象来创建LimitTracker
实例后,我们便可以通过调用set_value
方法检查模拟对象中是否存储了我们希望见到的消息。
//示例15-21 尝试实现MockMessenger,但在编译时无法通过借用检查
#[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
值的动态数组来记录所有接收到的信息。我们还定义了关联函数new
来方便地创建一个不包含任何消息的新MockMessenger实例。接着,我们为MockMessenger实现了Messenger trait,从而使它可以被用于创建LimitTracker。在send方法的定义中,参数中的文本会被存入sent_messages的MockMessenger列表。
在测试函数中,我们希望检查LimitTracker在当前值value超过最大max的75%时的行为。函数体中的代码首先创建了一个信息列表为空的MockMessenger实例,并使用它的引用及最大值100作为参数来创建LimitTrakcer。随后,我们调用了LimitTracker的set_value
方法,并将值80传入该方法,这个值超过了最大值100的75%。最后,我们断言MockMessenger的信息列表中存在一条被记录下来的信息。
由于send方法接收了self的不可变引用,所以我们不能修改MockMessenger的内容来记录消息。我们也无法按照编译器在错误提示信息中给出的建议来将函数签名修改为&mut self
,因为修改后的签名与Messenger trait
定义的send的签名不符。
在这个场景中,只要在RefCell<T>
中存入sent_messages
,send方法就可以修改sent_messages
来存储我们看到的信息:
//实例15-22:在保持外部值不可变的前提下,使用RefCell来修改内部存储的值
#[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() {
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.borrow().len(), 1);
}
}
sent_messages字段的类型变为了RefCell<Vec<String>>
,而不再是Vec<String>
。在new函数中,我们使用了一个空的动态数组来创建新的RefCell<Vec<String>>
实例。
对于send放的实现,其第一个参数依然是self的不可变借用,以便与trait的定义维持一致。随后的代码调用了RefCell<Vec<String>>
类型的self.sent_messages
的borrow_mut方法,来获取RefCell<Vec<String>>
内布置的可变引用。接着,我们便可以在动态数组的可变引用上调用push方法来存入数据,从而将已发送信息记录在案。
最后,为了查看内部动态素组的长度,我们需要先调用RefCell<Vec<String>>
的borrow方法来取得动态数组的不可变引用。
2.使用RefCell<T>在运行时记录借用信息
我们会在创建不可变和可变引用时分别使用语法&
和&mut
。对于RefCell<T>
而言,我们需要使用borrow
与borrow_mut
方法来实现类似的功能,这两者都被作为RefCell<T>
的安全接口来提供给用户。borrow方法和borrow_mut方法会分别返回Ref<T>
与RefMut<T>
这两种智能指针。由于这两种智能指针都实现了Deref,所以我们可以把它当作一般的引用来对待。
RefCell<T>
会记录当前存在多少个活跃的Ref<T>
和RefMut<T>
智能指针。每次调用borrow方法时,RefCell<T>
会将活跃的不可变借用计数加1。RefCell<T>
会基于这一技术来维护和编译器有一样的借用检查规则:在任何一个给定的时间里,它只允许你拥有多个不可变借用或一个可变借用。
当我们违背借用规则时,相比与一般引用导致的编译时错误,RefCell<T>
的实现会在运行时触发panic。
//实例15-23:修改了send函数。这段新代码故意在同一个作用域中创建两个同时有效的可变借用,以便演示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));
}
}
我们首先创建了一个RefMut<T>
类型的one_borrow变量来存储从borrow_mut
返回的结果,并在随后用同样的方法在two_borrow变量中创建另外一个可变借用。这段代码实现了一个不被允许的情形:同一个作用域中出现了两个可变引用。
注意,这段代码出发了panic并输出信息already borrowed: BorrowMutError
,这是RefCell<T>
在运行时处理违反借用规则代码的方法。
在运行时而不是编译时捕获借用错误意味着,开发者很有可能到研发后期才得以发现问题,甚至是将问题暴露到生产环境中。另外,代码也会因为运行时记录借用的数量而产生性能损失。但不管怎么样,使用RefCell<T>
都能够使我们在不可变的环境中修改自身数据,从而成功地编写出能够记录消息的不可变模拟对象。只要能够做出正确的取舍,你就可以借助RefCell<T>
来完成某些常规引用无法完成的功能。
3、将Rc<T>和RefCell<T>结合使用来实现一个拥有多重所有权的可变数据
将RefCell<T>
和Rc<T>
结合使用是一种常见用法。Rc<T>
允许多个所有者持有统一数据,但智能提供针对数据的不可变访问。如果我们在Rc<T>
内存储了RefCell<T>
,那么就可以定义出拥有多个所有者且能够进行修改的值。
我们以之前示例15-18中定义的链接列表为例,它使用Rc<T>
来让多个列表共享同一个列表的所有权。由于Rc<T>
只能存储不可变值,所以列表一经创建,其中的值就无法被再次修改。
#[derive(Debug)]
enum List {
Cons(Rc<RefCell<i32>>, Rc<List>),
Nil,
}
use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;
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(3)), Rc::clone(&a));
let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a));
*value.borrow_mut() += 10;
println!("a after = {:?}", a);
println!("b after = {:?}", b);
println!("c after = {:?}", c);
}
main函数中的代码首先创建了一个Rc<RefCell<i32>>
实例,并将它暂时存入了value变量中以便之后可以直接访问。接着,我们使用含有value的Cons变体创建一个List类型的a变量。为了确保a和value同时持有内部值5的所有权,这里的代码还克隆了value,而不仅仅只是将value的所有权传递给a,或者让a借用value。
为了让随后创建的b和c能够同时指向a,我们将a封装到了Rc<T>
中。创建完成a、b、c这三个列表后,我们通过调用borrow_mut来讲value指向的值增加10.注意,这里使用了自动解引用功能来将Rc<T>
解引用为RefCell<T>
。borrow_mut方法会返回一个RefMut<T>
智能指针,我们可以使用解引用运算符来修改内部值。
通过使用RefCell<T>
,我们拥有的List保持了表面上的不可变状态,并能够在必要时借用RefCell<T>
提供的方法来修改其内部存储的数据。运行时的借用规则检查同样能够帮助我们避免数据竞争,在某些场景下为了必要的灵活性而牺牲一些运行时性能也是值得的。
六、循环引用造成内存泄露
Rust提供的内存安全保障使我们很难在程序中意外地制造出永远不会得到释放的内存空间,但这也并非绝对的。与数据竞争不同,在编译器彻底防止内存泄漏并不是Rust做出的保证之一,这也意味着内存泄漏在Rust中使一种内存安全行为。你可以通过Rc<T>
和RefCell<T>
看到Rust是允许内存泄漏的:我们能够创建出互相引用成环状的实例。由于环中每一个指针的引用计数都不可能减少到0,所以对应的值也不会被释放丢弃,这就造成了内存泄漏。
1、创建循环引用
让我们看看循环引用是如何发生的,再来学习如何才能避免它。如下示例定义了一个List枚举,以及它的tail方法。
//示例15-25
fn main() {}
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枚举与示例15-5中的稍微有些区别。Cons变体的第二项元素变为了RefCell<Rc<List>>
,这也意味着我们现在可以灵活修改Cons变体指向的下一个List值。为了能够较为方便地访问Cons变体中的第二项元素,我们还专门添加了tail方法。
如下代码15-26代码首先建立了一个普通的列表a与一个指向a的列表b;随后,它又将列表a修改为指向b,如此便可以形成一个循环引用。中间添加的那些println!
可以让你观察到代码在运行各个阶段后引用计数的具体数值。
//示例15-26:构造一个循环引用,它由两个互相指向对方的List组成
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,
}
}
}
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 creationg = {}", 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));
//取消下面的注释行便可以观察到循环引用:它会造成栈的一处
//println!("a next item = {:?}", a.tail());
}
这段代码首先创造出一个Rc<List>
实例并存储至变量a,其中的List被赋予了初始值5,Nil。随后,我们又创建出一个Rc<List>
实例并存储值变量b,其中的List包含数值10及指向列表a的指针。
接下来,我们将a指向的下一个元素Nil修改为b来创建循环。为了实现这一修改,我们需要调用tail方法得到a的RefCell<Rc<List>>
值的引用并将它暂存在Link变量中。接着,我们使用RefCell<Rc<List>>
的borrow_mut方法来将Rc<List>
中存储的值由Nil修改为b中存储的Rc<List>
。
未取消注释情况下:
在完成a指向b的操作后,这两个Rc<List>
实例的引用计数就都变为了2。而在main函数结尾处,Rust会首先释放b,并使b存储的Rc<List>
实例的引用计数减少1。
但由于a仍然持有一个指向b中Rc<List>
的引用,所以这个Rc<List>
的引用计数仍然是1而不是0。因此,该Rc<List>
在堆上的内存不会得到释放。这块内存会永远以引用计数1的状态保留在堆上。
去除最后一行注释并再次运行程序,那么Rust会在尝试将这个循环引用打印出来的过程中反复地从a跳转到b,再从b跳转至a;整个程序会一致处于这种循环中知道发生栈溢出为止。
取消注释情况下:
在这个示例中,由于程序在创建循环引用后就立即结束运行了,所以它不会造成特别严重的后果,但是对于一个逻辑更复杂的程序而言,在循环引用中分配并长时间持有大量内存会让程序不断消耗掉超过业务所需的内存,这样的漏洞可能会导致内存逐步消耗殆尽并最后拖垮整个系统。
创建出循环引用意味着你的代码出现了bug,而这些bug可以通过自动化测试、代码评审及其他的软件开发手段来尽可能地避免。
另外一种用于解决循环引用的方法需要阻止数据结构,它会将引用拆分为持有所有权和不持有所有权两种情形。因此,你可以在形成的环状实例中让某些指向关系持有所有权,并让另外某些只想关系不持有所有权。只有持有所有权的指向关系才会影响到某个值是否能够被释放。
2、使用Weak<T>代替Rc<T>来避免循环引用
除了上述方法外,还可以通过调用Rc::downgrade
函数来创建出Rc<T>
的弱引用。使用Rc<T>
的引用调用Rc::downgrade
函数会返回一个类型为Weak<T>
的智能指针,这一操作会让Rc<T>
中的weak_cound
的计数增加1,而不会改变strong_count
的状态。Rc<T>
类型使用weak_count
来记录当前存在多少个Weak<T>
引用,这与strong_count
类似,但是它们之间的区别在于,Rc<T>
并不会执行清理操作前要求weak_count
必须减为0。
强引用可以被我们用来共享Rc<T>
实例的所有权,而弱引用则不会表达所有权关系。一旦强引用计数减为0,任何由弱引用组成的循环就会被打破。因此,弱引用不会造成循环引用。
由于我们无法确定Weak<T>
引用的值是否已经被释放了,所以我们需要在使用Weak<T>
指向的值之前确保它依然存在。你可以调用Weak<T>
实例的upgrade方法来完成这一验证。此函数返回的Option<Rc<T>>
值依然存在时表达为Some,而在Rc<T>
值被释放时表达为None。由于upgrade返回的是Option<T>
类型,所以Rust能够保证Some和None两个分支都得到妥善的处理,而不会产生无效指针之类的问题。
为了举例,我们放弃了仅仅指向下一个元素的列表结构,而会在接下来的示例中创建一棵树,它的每个节点都能够指向自己的父节点与全部的子节点。
1.创建树状数据结构:带有子节点的Node
首先,我们会创建一个能够指向字节的None结构体,它可以存储一个i32值及指向所有子节点的引用:
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
字段中使用RefCell<T>
包裹Vec<Rc<Node>>
来试下内部可变性。
接下来,我们将使用这个结构体定义一个值为3且没有子节点的Node实例,并将它作为叶子节点存入leaf变量。随后,我们还会再定义一个值为5且将leaf作为子节点的branch实例:
//示例15-27:创建leaf叶子节点和包含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
的引用,它甚至对两个节点直接存在父子关系的事实一无所知。
2.增加子节点指向父节点的引用
为了让子节点意识到父节点的存在,我们为Node结构体添加了一个parent
字段。而我们需要对parent
指定相关类型。但是Rc<T>
这个类型是不可以的,因为它会创建出循环引用。在branch.children
指向leaf
的同时使用leaf.parent
指向branch
会导致两者的strong_count
都无法归0。
考虑父子节点关系:父节点自然应该拥有子节点的所有权,因为当父节点被丢弃时,子节点也应当被随之丢弃。但子节点却不应该拥有父节点所有权,父节点的存在性不会因为丢弃子节点而受到影响。因此我们应当使用弱引用Weak<T>
。
新的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>>>,
}
,这样,节点便可以指向父节点却不持有它的所有权了。如下示例15-28根据这段定义更新了main函数,使leaf节点指向了自己的父节点branch。
//示例15-28:leaf节点持有一个指向父节点branch的弱引用
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());
}
除了parent
字段,创建leaf
节点的代码与之前区别不大。由于leaf
一开始不存在父节点,所以我们创建了一个空的Weak<Node>
引用实例来初始化parent
字段。
如果这个时候使用upgrade
方法来获得指向leaf
父节点的引用,那么我们就会得到一个None值。我们可以从第一个println!
语句输出中观察到这一现象:
因为branch
没有父节点,所以我们在创建branch
时将parent
字段同样设置为了一个空的Weak<Node>
引用。随后代码依然将leaf
用作了branch
的子节点。当branch
创建完毕后,我们就可以修改leaf
来增加指向父节点的Weak<Node>
引用了。为了实现这一目的,我们通过RefCell<Weak<Node>>
的borrow_mut
方法取出leaf
中parent
字段的可变借用。随后,我们使用Rc::downgrade
函数获取branch
中Rc<Node>
的Weak<Node>
引用,并将它存入leaf
的parent
字段中。
当我们再次打印leaf
的父节点时,便可以看到一个包含了branch
实际内容的Some
变体。这意味着leaf
现在可以访问父节点了,另外,现在打印leaf
还可以避免因循环引用而导致的栈溢出故障,因为Weak<Node>
引用会被直接打印为Weak
。
有限的输出意味着代码中没有产生循环引用。这一结论同样可以通过观察Rc::strong_count
和Rc::weak_count
的计数值来得出。
3.显示strong_count和weak_count计数值的变化
我们会将branch
的创建过程移动至一个新创建的内部作用域并显示strong_count和weak_count计数值的变化。我们通过这一实验观察到branch
在创建和丢弃时发生的操作。
//示例15-29:在内存作用域中创建branch并观察强引用和弱引用的计数
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在创建过程中克隆了leaf变量的Rc<Node>
,并将它存入了自己的branch.children中。此时,leaf的弱引用仍然为0。
当内部作用域结束时,branch会离开作用域并使Rc<Node>
的强引用计数减为0,从而导致该Node被丢弃。虽然branch的弱引用计数因为leaf.parent
的指向依然为1,但这并不会影响到Node是否会被丢弃,所以这段代码没有发生内存泄漏。
试图在作用域结束后访问leaf的父节点会得到一个Node值,当程序结束时,由于只有leaf变量指向了存储在自身中的Rc<Node>
,所以这个Rc<Node>
的强引用计数为1。
所有这些管理引用计数及值释放的逻辑都被封装到了Rc<T>
与Weak<T>
类型,以及它们对Drop trait的具体实现中。通过在Node定义中将子节点指向父节点的关系定义为一个Weak<T>
引用,可以使父子节点在指向彼此的同时避免产生循环引用或内存泄漏。