参考周期可能会泄漏内存
Rust 的内存安全保证使其难以(但并非不可能)
意外创建从未清理的内存(称为内存泄漏)。
完全防止内存泄漏并不是 Rust 的保证之一,这意味着
内存泄漏在 Rust 中是内存安全的。我们可以看到 Rust 允许内存泄漏
通过使用Rc<T>
和RefCell<T>
:可以在其中
项在一个循环中相互引用。这会产生内存泄漏,因为
循环中每个项目的引用计数永远不会达到 0,并且值
永远不会被丢弃。
创建引用循环
让我们看看引用循环是如何发生的以及如何防止它,
从List
enum 和tail
清单中的方法
15-25:
文件名: 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
variant 指的是
我们使用的是List
定义。这
second 元素中的Cons
variant 现在是RefCell<Rc<List>>
,这意味着
而不是能够修改i32
value 就像我们在 Listing 中所做的那样
15-24 中,我们要修改List
值 ACons
variant 指向。
我们还添加了一个tail
方法,以便我们访问
second item(如果我们有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:创建一个 2 的引用循环List
彼此指向的值
我们创建一个Rc<List>
实例持有List
value 在变量中a
初始列表为5, Nil
.然后,我们创建一个Rc<List>
实例持有
另一个List
value 在变量中b
,其中包含值 10 和 Points
到a
.
我们修改a
所以它指向b
而不是Nil
,创建循环。我们这样做
通过使用tail
方法获取对RefCell<Rc<List>>
在a
,我们将其放入变量link
.然后我们使用borrow_mut
方法上的RefCell<Rc<List>>
要将Rc<List>
,它持有一个Nil
值设置为Rc<List>
在b
.
当我们运行这段代码时,保留最后一个println!
注释掉了
moment,我们将得到这个输出:
$ 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
的Rc<List>
实例a
和b
在 2 之后
我们将a
指向b
.在main
中,Rust 会删除
变量b
,这会减少b
Rc<List>
实例
从 2 到 1。内存Rc<List>
has 在堆上不会被丢弃
这一点,因为它的引用计数是 1,而不是 0。然后 Rust 下降a
哪
减少a
Rc<List>
实例从 2 到 1 作为
井。此实例的内存也无法删除,因为另一个Rc<List>
instance 仍然引用它。分配给列表的内存将
永远未收集。为了可视化这个引用循环,我们创建了一个
图 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<T>
实例Rc::downgrade
并传递一个
对Rc<T>
.强引用是您共享
一Rc<T>
实例。弱引用不表达所有权关系,
并且它们的计数不会影响Rc<T>
实例被清理。他们
不会导致引用循环,因为任何涉及一些弱引用的循环
一旦涉及的值的强引用计数为 0,就会被中断。
当您调用Rc::downgrade
中,您将获得Weak<T>
.
而不是增加strong_count
在Rc<T>
实例按 1 调用Rc::downgrade
增加weak_count
按 1.这Rc<T>
type 使用weak_count
跟踪数量Weak<T>
引用存在,类似于strong_count
.区别在于weak_count
不需要为 0,因为Rc<T>
实例。
因为Weak<T>
引用可能已被删除,以执行
任何值为Weak<T>
指向 的
值仍然存在。通过调用upgrade
方法在Weak<T>
实例,它将返回一个Option<Rc<T>>
.您将获得Some
如果Rc<T>
value 尚未被丢弃,并且None
如果Rc<T>
值已被删除。因为upgrade
返回一个Option<Rc<T>>
,
Rust 将确保Some
case 和None
case 的
不会有无效的指针。
例如,而不是使用其项只知道下一个 item 中,我们将创建一个树,其 items 了解其子 items 和父 items。
创建树数据结构:一个Node
与子节点
首先,我们将构建一个包含知道其子节点的节点的树。
我们将创建一个名为Node
这有自己的i32
value 以及
对其子项的引用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>
items 为 type 为Rc<Node>
.我们还希望
修改哪些节点是另一个节点的子节点,这样我们就有了一个RefCell<T>
在children
周围Vec<Rc<Node>>
.
接下来,我们将使用我们的结构体定义并创建一个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
没有子节点的节点
以及branch
node 替换为leaf
作为其子项之一
我们克隆Rc<Node>
在leaf
并将其存储在branch
,表示Node
在leaf
现在有两个所有者:leaf
和branch
.我们可以从branch
自leaf
通过branch.children
,但是没有办法从leaf
自branch
.原因是leaf
没有引用branch
和
不知道他们是有关系的。我们希望leaf
要知道branch
是其
父母。我们接下来会这样做。
添加从 Child 到 Parent 的引用
要使子节点知道其父节点,我们需要添加一个parent
field 设置为
我们Node
struct 定义。问题在于决定parent
应该是。我们知道它不能包含Rc<T>
,因为那样
创建参考循环leaf.parent
指向branch
和branch.children
指向leaf
,这会导致其strong_count
值永远不会为 0。
从另一个角度考虑关系,父节点应该拥有其 children:如果删除了父节点,则应将其子节点删除为 井。但是,子节点不应拥有其父节点:如果我们删除子节点,则 parent 应该仍然存在。这是弱引用的情况!
所以而不是Rc<T>
,我们将 type 为parent
用Weak<T>
,
具体来说,是RefCell<Weak<Node>>
.现在我们的Node
结构体定义 looks
喜欢这个:
文件名: 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
节点,并且对其
父节点branch
创建leaf
node 看起来类似于示例 15-27,除了
这parent
田:leaf
开始时没有父级,因此我们创建一个新的
空Weak<Node>
reference 实例。
此时,当我们尝试获取对leaf
通过使用
这upgrade
方法,我们得到一个None
价值。我们在
第一println!
陈述:
leaf parent = None
当我们创建branch
节点中,它还将有一个新的Weak<Node>
引用在parent
字段,因为branch
没有父节点。
我们仍然有leaf
作为branch
.一旦我们有了Node
实例branch
,我们可以修改leaf
给它一个Weak<Node>
引用其父级。我们使用borrow_mut
方法上的RefCell<Weak<Node>>
在parent
字段为leaf
,然后使用Rc::downgrade
函数创建Weak<Node>
参考branch
从
这Rc<Node>
在branch.
当我们打印 的父级leaf
同样,这次我们将得到一个Some
变体
占有branch
:现在leaf
可以访问其父级!当我们打印时leaf
我们
还要避免最终以堆栈溢出告终的循环,就像我们在
示例 15-26;这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
让我们看看strong_count
和weak_count
的值Rc<Node>
实例通过创建新的内部作用域并将branch
进入该范围。通过这样做,我们可以看到当branch
是
created 并在超出范围时删除。显示修改
在示例 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:创建branch
in an inner scope 和
检查强参考计数和弱参考计数
后leaf
时,其Rc<Node>
具有强计数 1 和弱计数
计数为 0。在内部作用域中,我们创建branch
并将其与leaf
,此时当我们打印计数时,Rc<Node>
在branch
将具有强计数 1 和弱计数 1(对于leaf.parent
指点
自branch
替换为Weak<Node>
).当我们在leaf
,我们会看到
它将具有 2 的强计数,因为branch
现在拥有Rc<Node>
之leaf
存储于branch.children
,但仍然会有一个较弱的
计数为 0。
当内部范围结束时,branch
超出范围,并且
这Rc<Node>
减少到 0,因此其Node
被丢弃。弱计数 1
从leaf.parent
与是否Node
被丢弃,因此我们
不要泄漏任何内存!
如果我们尝试访问leaf
范围结束后,我们将得到None
再。在程序结束时,Rc<Node>
在leaf
具有很强的
count 为 1,弱 count 为 0,因为变量leaf
现在是唯一的
对Rc<Node>
再。
管理计数和值删除的所有逻辑都内置于Rc<T>
和Weak<T>
及其Drop
特性。由
指定从 child 到 parent 的关系应该是Weak<T>
定义中的引用Node
,您可以拥有 parent
节点指向子节点,反之亦然,无需创建引用循环
和内存泄漏。
总结
本章介绍了如何使用智能指针进行不同的保证和
与 Rust 默认使用常规引用进行的权衡。这Box<T>
type 具有已知大小,并指向在堆上分配的数据。这Rc<T>
type 跟踪对堆上数据的引用数,因此
该数据可以有多个所有者。这RefCell<T>
类型及其内部
可变性为我们提供了一个类型,当我们需要一个不可变类型时,我们可以使用它,但是
需要更改该类型的内部值;它还强制执行借用
规则。
还讨论了Deref
和Drop
trait 中实现的许多
智能指针的功能。我们探讨了可能导致
内存泄漏以及如何使用Weak<T>
.
如果本章激起了您的兴趣,并且您想实施自己的 智能指针,请查看 “The Rustonomicon” 了解更多有用 信息。
接下来,我们将讨论 Rust 中的并发性。您甚至会了解一些新的 智能指针。
本文档由官方文档翻译而来,如有差异请以官方英文文档(https://doc.rust-lang.org/)为准