参考周期可能会泄漏内存
Rust 的内存安全保证使得意外创建从未清理的内存(称为内存泄漏)变得困难,但并非不可能。完全防止内存泄漏并不是 Rust 的保证之一,这意味着内存泄漏在 Rust 中是内存安全的。我们可以看到 Rust 通过使用 Rc<T>
和 RefCell<T>
来允许内存泄漏:可以创建在一个循环中相互引用的引用。这会产生内存泄漏,因为循环中每个项目的引用计数永远不会达到 0,并且这些值永远不会被丢弃。
创建引用循环
让我们看看引用循环是如何发生的以及如何防止它,从示例 15-25 中 List
枚举和 tail
方法的定义开始:
文件名: src/main.rs
use crate::List::{Cons, Nil}; use std::cell::RefCell; use std::rc::Rc; #[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() {}
示例 15-25:一个 cons list 定义,其中包含一个
RefCell<T>
,以便我们可以修改 Cons
变体所指的内容
我们使用的是示例 15-5 中 List
定义的另一种变体。Cons
变体中的第二个元素现在是 RefCell<Rc<List>>
,这意味着我们不是像示例 15-24 中那样能够修改 i32
值,而是想要修改 Cons
变体指向的 List
值。我们还添加了一个 tail
方法,以便于我们在有 Cons
变体时访问第二个项目。
在示例 15-26 中,我们添加了一个 main
函数,该函数使用示例 15-25 中的定义。此代码在 a
中创建一个列表,在 b
中创建一个列表,该列表指向 a
中的列表。然后,它将 a
中的列表修改为指向 b
,从而创建一个引用循环。沿途有 println!
语句来显示在此过程中不同时间点的引用计数。
文件名: src/main.rs
use crate::List::{Cons, Nil}; use std::cell::RefCell; use std::rc::Rc; #[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()); }
示例 15-26:创建两个 List
的引用循环
彼此指向的值
我们创建一个 Rc<List>
实例,在变量 a
中保存一个 List
值
初始列表为 5,无
。然后,我们创建一个 Rc<List>
实例,其中包含变量 b
中的另一个 List
值,该值包含值 10 并指向 a
中的列表。
我们修改 a
,使其指向 b
而不是 Nil
,从而创建一个循环。我们通过使用 tail
方法获取对 RefCell<Rc<List>>
的引用来实现这一点
in a
中,我们将其放入变量 link
.然后我们使用 borrow_mut
RefCell<Rc<List>>
上的方法,以更改 Rc<List>
中的值
,它对 b
中的 Rc<List>
保持 Nil
值。
当我们运行这段代码时,暂时保留最后一个 println!
注释掉,我们将得到以下输出:
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.53s
Running `target/debug/cons-list`
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>
实例的引用计数从 2 减少到 1。此时 Rc<List>
在堆上的内存不会被丢弃,因为它的引用计数是 1,而不是 0。然后 Rust 丢弃 a
,这会将 a Rc<List>
实例的引用计数从 2 减少到 1,因为
井。此实例的内存也无法删除,因为另一个
Rc<List>
实例仍然引用它。分配给列表的内存将永远保持未收集状态。为了直观地显示此引用循环,我们在图 15-4 中创建了一个图表。
图 15-4:列表 a
和 b
的参考循环
彼此指向
如果你取消对最后一个 println!
的注释并运行该程序,Rust 将尝试打印这个循环,其中 a
指向 b
,指向 a
,依此类推,直到它溢出堆栈。
与实际程序相比,在此示例中创建引用循环的后果并不是很可怕:在我们创建引用循环后,程序立即结束。但是,如果更复杂的程序在一个周期中分配了大量内存并长时间持有它,则该程序将使用比所需更多的内存,并且可能会使系统不堪重负,从而导致其耗尽可用内存。
创建参考循环并不容易,但也并非不可能。如果您的 RefCell<T>
值包含 Rc<T>
值或类似的嵌套类型组合,则必须确保不创建循环;你不能指望 Rust 来捕捉它们。创建引用循环将是程序中的一个逻辑错误,您应该使用自动测试、代码审查和其他软件开发实践来尽量减少它。
避免引用循环的另一种解决方案是重新组织数据结构,以便某些引用表示所有权,而某些引用不表示所有权。因此,您可以拥有由一些所有权关系和一些非所有权关系组成的周期,并且只有所有权关系会影响是否可以删除值。在示例 15-25 中,我们总是希望 Cons
variants 来拥有其列表,因此无法重新组织数据结构。
让我们看一个使用由父节点和子节点组成的图形的示例
查看何时使用非所有权关系是防止
引用循环。
防止参考循环:将 Rc<T>
转换为 Weak<T>
到目前为止,我们已经证明了调用 Rc::clone
会增加
strong_count
Rc<T>
实例,并且仅当 Rc<T>
实例的strong_count
为 0 时才会被清理。您还可以通过调用 Rc::d owngrade
并传递对 Rc<T>
的引用,在 Rc<T>
实例中创建对该值的弱引用。强引用是共享 Rc<T>
实例的所有权的方式。弱引用不表示所有权关系,并且它们的计数不会影响何时清理 Rc<T>
实例。它们不会导致引用循环,因为一旦涉及值的强引用计数为 0,任何涉及一些弱引用的循环都会被打破。
当您调用 Rc::d owngrade
时,您将获得一个 Weak<T>
类型的智能指针。不是将 Rc<T>
实例中的 strong_count
增加 1,而是调用
Rc::d owngrade
将 weak_count
增加 1。Rc<T>
类型使用
weak_count
来跟踪存在多少个 Weak<T>
引用,类似于
strong_count
。区别在于 weak_count
不需要为 0
要清理的 Rc<T>
实例。
由于 Weak<T>
引用的值可能已被删除,因此若要对 Weak<T>
指向的值执行任何作,必须确保该值仍然存在。通过在 Weak<T>
上调用 upgrade
方法来执行此作
实例,它将返回一个 Option<Rc<T>>
。你会得到一个 Some
的结果
如果尚未删除 Rc<T>
值,并且如果
已删除 Rc<T>
值。因为 upgrade
返回一个 Option<Rc<T>>
,所以 Rust 将确保 Some
和 None
的情况被处理,并且不会有无效的指针。
例如,我们将创建一个树,其中的 items 知道其子项和
它们的父项。
创建树数据结构:具有子节点的节点
首先,我们将构建一个包含知道其子节点的节点的树。我们将创建一个名为 Node
的结构体,该结构体包含自己的 i32
值以及对其子 Node
值的引用:
文件名: src/main.rs
use std::cell::RefCell; use std::rc::Rc; #[derive(Debug)] struct Node { value: i32, children: RefCell<Vec<Rc<Node>>>, } 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)]), }); }
我们希望 Node
拥有它的子节点,并且我们希望与变量共享该所有权,以便我们可以直接访问树中的每个 Node
。为此,我们将 Vec<T>
项目定义为 Rc<Node>
类型的值。我们还想修改哪些节点是另一个节点的子节点,因此我们在
Vec<Rc<Node>>
周围的孩子
。
接下来,我们将使用我们的结构体定义并创建一个名为
leaf 的 3
且没有 children,以及另一个名为 branch
的实例
其中,值为 5 和 leaf
作为其子项之一,如示例 15-27 所示:
文件名: src/main.rs
use std::cell::RefCell; use std::rc::Rc; #[derive(Debug)] struct Node { value: i32, children: RefCell<Vec<Rc<Node>>>, } 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)]), }); }
示例 15-27:创建一个没有子节点的 leaf
节点和一个以 leaf
作为其子节点之一的 branch
节点
我们在叶
子中克隆 Rc<Node>
,并将其存储在 branch
中,这意味着
叶
中的 Node
现在有两个所有者:leaf
和 branch
。我们可以从
branch
到 Leaf
的 branch.children
,但没有办法从
叶
子到树枝
。原因是 leaf
没有引用 branch
,也不知道它们是相关的。我们希望 leaf
知道 branch
是它的父级。我们接下来会这样做。
添加从 Child 到 Parent 的引用
要使子节点知道其父节点,我们需要将父
字段添加到我们的 Node
结构体定义中。问题在于决定
parent
应该是。我们知道它不能包含 Rc<T>
,因为这会创建一个引用循环,其中 leaf.parent
指向 branch
,而
branch.children
指向 leaf
,这将导致他们的 strong_count
值永远不会为 0。
从另一个角度考虑关系,父节点应该拥有它的子节点:如果删除了父节点,则其子节点也应该被删除。但是,子节点不应拥有其父节点:如果我们删除子节点,父节点应该仍然存在。这是弱引用的情况!
因此,我们将父级
类型使用 Weak<T>
,而不是 Rc<T>
,特别是 RefCell<Weak<Node>>
。现在我们的 Node
结构体定义如下所示:
文件名: src/main.rs
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() { 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()); }
节点将能够引用其父节点,但不拥有其父节点。在示例 15-28 中,我们更新了 main
以使用这个新定义,因此 leaf
node 将有一种方法来引用它的父级 branch
:
文件名: src/main.rs
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() { 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()); }
示例 15-28:对其父节点分支
的弱引用的叶节点
创建 leaf
节点看起来类似于示例 15-27,除了 parent
字段:leaf
开始时没有父节点,因此我们创建一个新的空 Weak<Node>
引用实例。
此时,当我们尝试使用 upgrade
方法获取对 leaf
的父级的引用时,我们会得到一个 None
值。我们在第一个 println!
语句的输出中看到了这一点:
叶父级 = 无
当我们创建分支
节点时,它还将具有一个新的 Weak<Node>
引用,因为
Branch
没有父节点。我们仍然将 leaf
作为 branch
的子节点之一。一旦我们有了
Node
实例,我们可以修改
叶
子,给它一个 Weak<Node>
引用其父级。borrow_mut
我们在
RefCell<Weak<Node>>
的父
字段中,然后
我们使用
Rc::d owngrade
函数创建对分支
中 Rc<Node>
分支的 Weak<Node>
引用。
当我们再次打印 leaf
的父级时,这次我们将得到一个 Some
variant 持有分支
:现在 leaf
可以访问它的父级!当我们打印 leaf
时,我们也避免了最终以堆栈溢出告终的循环,就像示例 15-26 中那样;弱<Node>
引用打印为 (弱)
:
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
的更改
让我们看看 Rc<Node>
的 strong_count
和 weak_count
值如何
实例通过创建新的内部作用域并将
分支
到该范围。通过这样做,我们可以看到当 branch
被创建,然后在它超出范围时被丢弃时会发生什么。修改如示例 15-29 所示:
文件名: src/main.rs
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() { 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), ); }
示例 15-29:在内部范围内创建分支
并检查强引用和弱引用计数
创建叶
子后,其 Rc<Node>
的强计数为 1,弱计数为 0。在内部作用域中,我们创建 branch
并将其与
叶
子,此时当我们打印计数时,分支
中的 Rc<Node>
将具有强计数 1 和弱计数 1(对于 leaf.parent
指向具有 Weak<Node>
的分支
)。当我们在 leaf
中打印计数时,我们会看到它的计数为 2,因为 branch
现在有一个
Rc<Node>
的叶
子存储在 branch.children
中,但仍然具有弱计数 0。
当内部范围结束时,branch
超出范围,并且 Rc<Node>
的强计数减少到 0,因此其 Node
被丢弃。leaf.parent
的弱计数 1 与是否删除 Node
无关,因此我们不会收到任何内存泄漏!
如果我们尝试在范围结束后访问 leaf
的父级,我们将得到
再也没有
。在程序结束时,叶
中的 Rc<Node>
具有强计数 1 和弱计数 0,因为变量叶
现在再次成为对 Rc<Node>
的唯一引用。
管理计数和值删除的所有逻辑都内置于
Rc<T>
和 Weak<T>
及其 Drop
特性的实现。由
指定从 child 到 parent 的关系应该是
Weak<T>
引用,则可以让父节点指向子节点,反之亦然,而不会创建引用循环和内存泄漏。
总结
本章介绍了如何使用智能指针进行不同的保证和
与 Rust 默认使用常规引用进行的权衡。这
Box<T>
类型具有已知大小,并指向堆上分配的数据。这
Rc<T>
类型跟踪对堆上数据的引用数,以便数据可以有多个所有者。RefCell<T>
类型及其内部可变性为我们提供了一个类型,当我们需要一个不可变类型但需要更改该类型的内部值时,我们可以使用该类型;它还在运行时而不是在编译时强制执行借用规则。
还讨论了 Deref
和 Drop
traits,它们支持智能指针的许多功能。我们探讨了可能导致内存泄漏的参考周期,以及如何使用 Weak<T>
来防止它们。
如果本章激起了您的兴趣,并且您想实现自己的智能指针,请查看 “The Rustonomicon” 以获取更多有用的信息。
接下来,我们将讨论 Rust 中的并发性。您甚至会了解一些新的智能指针。