参考周期可能会泄漏内存
Rust 的内存安全保证使其难以(但并非不可能)
意外创建从未清理的内存(称为内存泄漏)。
完全防止内存泄漏并不是 Rust 的保证之一,这意味着
内存泄漏在 Rust 中是内存安全的。我们可以看到 Rust 允许内存泄漏
通过使用 and : 可以创建引用,其中
项在一个循环中相互引用。这会产生内存泄漏,因为
循环中每个项目的引用计数永远不会达到 0,并且值
永远不会被丢弃。Rc<T>
RefCell<T>
创建引用循环
让我们看看引用循环是如何发生的以及如何防止它,
从 枚举的定义和 清单 中的方法开始
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-5 中定义的另一个变体。这
变体中的第二个元素是 now ,这意味着
而不是像我们在 清单 中那样能够修改值
15-24 中,我们想要修改 variant 指向的值。
我们还添加了一个方法,以便于我们访问
second item(如果我们有变体)。List
Cons
RefCell<Rc<List>>
i32
List
Cons
tail
Cons
在示例 15-26 中,我们添加了一个函数,该函数使用
示例 15-25.此代码在 中创建了一个 list in 和一个指向
中的列表。然后,它将 中的列表修改为 指向 ,创建一个
参考循环。沿途有一些语句可以显示
引用计数处于此过程的不同时间点。main
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()); }
我们创建一个实例,其中包含变量中的值,初始列表为 。然后,我们创建一个持有
变量中包含值 10 和 Points 的另一个值
到 中的列表。Rc<List>
List
a
5, Nil
Rc<List>
List
b
a
我们修改它,使其指向而不是 ,从而创建一个循环。我们这样做
通过使用该方法获取对 in 的引用,我们将其放入变量 .然后我们使用 the 上的方法将 inside 的值从 an 更改为 in 。a
b
Nil
tail
RefCell<Rc<List>>
a
link
borrow_mut
RefCell<Rc<List>>
Rc<List>
Nil
Rc<List>
b
当我们运行此代码时,保留最后一个注释掉的
moment,我们将得到这个输出: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
两者中实例的引用计数 和 后面为 2
我们将 中的列表更改为 指向 。在 的末尾,Rust 会丢弃
variable ,这会减少实例的引用计数
从 2 到 1。堆上的内存不会在
这一点,因为它的引用计数是 1,而不是 0。然后 Rust drops ,它
将实例的引用计数从 2 减少到 1,如
井。此实例的内存也无法删除,因为另一个实例仍引用它。分配给列表的内存将
永远未收集。为了可视化这个引用循环,我们创建了一个
图 15-4 中的图表。Rc<List>
a
b
a
b
main
b
b
Rc<List>
Rc<List>
a
a
Rc<List>
Rc<List>
如果你取消注释最后一个并运行该程序,Rust 将尝试
打印此循环,并指向指向,依此类推,直到它
溢出堆栈。println!
a
b
a
与实际程序相比,创建参考循环的后果 在这个例子中不是很可怕:在我们创建引用循环之后, 程序结束。但是,如果更复杂的程序分配了大量内存 在一个循环中并长时间保持它,程序将使用更多的内存 超过它需要的,并且可能会使系统不堪重负,导致它耗尽 可用内存。
创建参考循环并不容易,但也并非不可能。
如果您的值包含值或类似的嵌套
类型与内部可变性和引用计数的组合,您必须
确保你不创建周期;你不能指望 Rust 来捕捉它们。
创建引用循环将是程序中的一个逻辑错误,您应该这样做
使用自动化测试、代码审查和其他软件开发实践来
最小化。RefCell<T>
Rc<T>
避免引用循环的另一种解决方案是重新组织数据
结构,以便某些引用表示所有权,而某些引用则不表示所有权。
因此,您可以拥有由一些所有权关系和
某些非所有权关系,并且只有所有权关系会影响
是否可以删除值。在示例 15-25 中,我们总是希望变体拥有自己的列表,因此重组数据结构是不可能的。
让我们看一个使用由父节点和子节点组成的图形的示例
查看何时使用非所有权关系是防止
引用循环。Cons
防止参考循环:将 Rc<T>
转换为 Weak<T>
到目前为止,我们已经证明了调用 increase of a instance,并且只清理一个实例
如果它是 0,则为 up。您还可以创建对
value 中,通过调用并传递
对 .强引用是您共享
一个实例。弱引用不表达所有权关系,
并且它们的计数不会影响何时清理实例。他们
不会导致引用循环,因为任何涉及一些弱引用的循环
一旦涉及的值的强引用计数为 0,就会被中断。Rc::clone
strong_count
Rc<T>
Rc<T>
strong_count
Rc<T>
Rc::downgrade
Rc<T>
Rc<T>
Rc<T>
调用 时,您将获得 类型的智能指针。
调用将实例中的 the 增加 1,而不是将 the 增加 1。该类型用于跟踪存在多少个引用,类似于 .区别在于 does not need to be 0 for the instance be cleanup。Rc::downgrade
Weak<T>
strong_count
Rc<T>
Rc::downgrade
weak_count
Rc<T>
weak_count
Weak<T>
strong_count
weak_count
Rc<T>
因为引用的值可能已被删除,所以要执行
任何具有 a 所指向的值的 API 的 API 中,都必须确保
值仍然存在。通过对实例调用方法来执行此作,该方法将返回一个 .如果值尚未被删除,您将获得 result of 如果值已被删除。因为返回一个 ,
Rust 将确保 case 和 case 得到处理,并且
不会有无效的指针。Weak<T>
Weak<T>
upgrade
Weak<T>
Option<Rc<T>>
Some
Rc<T>
None
Rc<T>
upgrade
Option<Rc<T>>
Some
None
例如,而不是使用其项只知道下一个 item 中,我们将创建一个树,其 items 了解其子 items 和父 items。
创建树数据结构:具有子节点的节点
首先,我们将构建一个包含知道其子节点的节点的树。
我们将创建一个名为 struct name,它包含自己的值以及
对其 children 值的引用: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)]), }); }
我们希望 a 拥有自己的子项,并且我们希望与 a 共享该所有权
变量,这样我们就可以直接在树中访问每个变量。为此,我们
将项目定义为 类型的值。我们还希望
修改哪些节点是另一个节点的子节点,这样我们在 .Node
Node
Vec<T>
Rc<Node>
RefCell<T>
children
Vec<Rc<Node>>
接下来,我们将使用我们的结构体定义,创建一个名为 3 且没有 children 的实例,以及另一个名为 5 的实例,作为其 children 之一,如示例 15-27 所示:Node
leaf
branch
leaf
文件名: 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)]), }); }
我们克隆 in 并将其存储在 中,这意味着 in 现在有两个所有者:和 。我们可以从 到 到 ,但没有办法从 到 。原因是没有引用 和
不知道他们是有关系的。我们想知道的是它的
父母。我们接下来会这样做。Rc<Node>
leaf
branch
Node
leaf
leaf
branch
branch
leaf
branch.children
leaf
branch
leaf
branch
leaf
branch
添加从 Child 到 Parent 的引用
要使子节点知道其父节点,我们需要在
我们的 struct 定义。问题在于决定应该是什么类型。我们知道它不能包含 , 因为那样会
创建一个指向 和 指向 的引用循环,这将导致它们的值永远不会为 0。parent
Node
parent
Rc<T>
leaf.parent
branch
branch.children
leaf
strong_count
从另一个角度考虑关系,父节点应该拥有其 children:如果删除了父节点,则应将其子节点删除为 井。但是,子节点不应拥有其父节点:如果我们删除子节点,则 parent 应该仍然存在。这是弱引用的情况!
所以,我们将 type of use ,
具体来说,就是 .现在,我们的结构体定义看起来
喜欢这个:Rc<T>
parent
Weak<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 中,我们更新为使用这个新定义,因此 node 将有办法引用它的父节点:main
leaf
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-27,除了
field: 开始时没有父级,因此我们创建了一个新的
空引用实例。leaf
parent
leaf
Weak<Node>
此时,当我们尝试使用 获取对 父级的引用时
方法,我们得到一个值。我们在
第一个语句:leaf
upgrade
None
println!
leaf parent = None
当我们创建节点时,它还将在字段中有一个新的引用,因为它没有父节点。
我们仍然作为 .一旦我们在 中拥有实例,我们就可以对其进行修改,以为其提供对其父级的引用。我们在 的字段中使用 的方法 ,然后使用该函数创建对 from 的引用
在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.
当我们再次打印 的父级时,这次我们将得到一个 variant
按住 : 现在可以访问其父级!当我们打印时,我们
还要避免最终以堆栈溢出告终的循环,就像我们在
示例 15-26;参考文献打印为:leaf
Some
branch
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
的更改
让我们看看实例的 and 值是如何变化的,方法是创建一个新的内部作用域并将 的创建移动到该作用域中。通过这样做,我们可以看到当
created 并在超出范围时删除。显示修改
在示例 15-29 中:strong_count
weak_count
Rc<Node>
branch
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 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), ); }
创建后,其具有强计数 1 和弱计数
计数为 0。在内部作用域中,我们创建并将其与 关联,此时当我们打印计数时,in 将具有强计数 1 和弱计数 1(用于指向
to 替换为 )。当我们在 中打印计数时,我们会看到
它将具有强计数 2,因为现在有 of 的克隆存储在 中,但仍然会有一个弱
计数为 0。leaf
Rc<Node>
branch
leaf
Rc<Node>
branch
leaf.parent
branch
Weak<Node>
leaf
branch
Rc<Node>
leaf
branch.children
当内部范围结束时,超出范围,并且
减少到 0,因此 its 被丢弃。弱计数 1
from 与是否被丢弃无关,因此我们
不要泄漏任何内存!branch
Rc<Node>
Node
leaf.parent
Node
如果我们尝试在 scope 结束后访问 parent of,我们将再次获得。在程序结束时,in 具有很强的
count 为 1,弱 count 为 0,因为该变量现在是唯一的
引用 again.leaf
None
Rc<Node>
leaf
leaf
Rc<Node>
所有管理 counts 和 value dropping 的 logic 都内置于 trait 及其实现中。由
指定从 child 到 parent 的关系应该是 定义中的引用,则可以拥有 parent
节点指向子节点,反之亦然,无需创建引用循环
和内存泄漏。Rc<T>
Weak<T>
Drop
Weak<T>
Node
总结
本章介绍了如何使用智能指针进行不同的保证和
与 Rust 默认使用常规引用进行的权衡。该类型具有已知大小,并指向在堆上分配的数据。该类型跟踪对堆上数据的引用数,因此
该数据可以有多个所有者。类型及其内部
可变性为我们提供了一个类型,当我们需要一个不可变类型时,我们可以使用它,但是
需要更改该类型的内部值;它还强制执行借用
规则。Box<T>
Rc<T>
RefCell<T>
还讨论了 和 trait,它们支持许多
智能指针的功能。我们探讨了可能导致
内存泄漏以及如何使用 .Deref
Drop
Weak<T>
如果本章激起了您的兴趣,并且您想实施自己的 智能指针,请查看 “The Rustonomicon” 了解更多有用 信息。
接下来,我们将讨论 Rust 中的并发性。您甚至会了解一些新的 智能指针。
本文档由官方文档翻译而来,如有差异请以官方英文文档(https://doc.rust-lang.org/)为准