x-i18n: generated_at: “2026-03-01T14:37:07Z” model: gemini-3-flash-preview provider: google-gemini-cli source_hash: 63e59efb318ea3cf88df11b3f87528bd221abfd25174f8a0388a445c41018e71 source_path: ch15-06-reference-cycles.md workflow: 16
引用循环可能导致内存泄漏 (Reference Cycles Can Leak Memory)
Reference Cycles Can Leak Memory
Rust 的内存安全保证使其很难(但并非不可能)意外地创建永远不会被清理的内存(即“内存泄漏 (memory leak)”)。完全防止内存泄漏并不是 Rust 的保证之一,这意味着内存泄漏在 Rust 中是内存安全的。我们可以看到,通过使用 Rc<T> 和 RefCell<T> ,Rust 允许发生内存泄漏:可以创建项与项之间循环引用的引用。这会产生内存泄漏,因为循环中每个项的引用计数永远不会达到 0,值也永远不会被丢弃。
Rust’s memory safety guarantees make it difficult, but not impossible, to
accidentally create memory that is never cleaned up (known as a memory leak).
Preventing memory leaks entirely is not one of Rust’s guarantees, meaning
memory leaks are memory safe in Rust. We can see that Rust allows memory leaks
by using Rc<T> and RefCell<T>: It’s possible to create references where
items refer to each other in a cycle. This creates memory leaks because the
reference count of each item in the cycle will never reach 0, and the values
will never be dropped.
创建引用循环 (Creating a Reference Cycle)
让我们看看引用循环是如何发生的,以及如何防止它,首先从示例 15-25 中的 List 枚举定义和 tail 方法开始。
Let’s look at how a reference cycle might happen and how to prevent it,
starting with the definition of the List enum and a tail method in Listing
15-25.
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-25/src/main.rs:here}}
}
我们使用的是示例 15-5 中 List 定义的另一个变体。 Cons 变体中的第二个元素现在是 RefCell<Rc<List>> ,这意味着与其像我们在示例 15-24 中那样具有修改 i32 值的能力,我们现在想修改 Cons 变体指向的 List 值。我们还添加了一个 tail 方法,以便在拥有 Cons 变体时方便地访问第二个项。
We’re using another variation of the List definition from Listing 15-5. The
second element in the Cons variant is now RefCell<Rc<List>>, meaning that
instead of having the ability to modify the i32 value as we did in Listing
15-24, we want to modify the List value a Cons variant is pointing to.
We’re also adding a tail method to make it convenient for us to access the
second item if we have a Cons variant.
在示例 15-26 中,我们添加了一个 main 函数,它使用示例 15-25 中的定义。这段代码在 a 中创建了一个列表,在 b 中创建了一个指向 a 中列表的列表。然后,它修改 a 中的列表以指向 b ,从而创建一个引用循环。在此过程中有 println! 语句来显示该过程各个点的引用计数。
In Listing 15-26, we’re adding a main function that uses the definitions in
Listing 15-25. This code creates a list in a and a list in b that points to
the list in a. Then, it modifies the list in a to point to b, creating a
reference cycle. There are println! statements along the way to show what the
reference counts are at various points in this process.
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-26/src/main.rs:here}}
}
我们在变量 a 中创建了一个持有 List 值的 Rc<List> 实例,初始列表为 5, Nil 。然后我们在变量 b 中创建了另一个持有 List 值的 Rc<List> 实例,它包含值 10 并且指向 a 中的列表。
We create an Rc<List> instance holding a List value in the variable a
with an initial list of 5, Nil. We then create an Rc<List> instance holding
another List value in the variable b that contains the value 10 and
points to the list in a.
我们修改 a 使其指向 b 而非 Nil ,从而创建一个循环。我们通过使用 tail 方法获取 a 中 RefCell<Rc<List>> 的引用来实现这一点,并将其放入变量 link 中。然后,我们在 RefCell<Rc<List>> 上使用 borrow_mut 方法,将其内部的值从持有 Nil 值的 Rc<List> 更改为 b 中的 Rc<List> 。
We modify a so that it points to b instead of Nil, creating a cycle. We
do that by using the tail method to get a reference to the
RefCell<Rc<List>> in a, which we put in the variable link. Then, we use
the borrow_mut method on the RefCell<Rc<List>> to change the value inside
from an Rc<List> that holds a Nil value to the Rc<List> in b.
当我们运行这段代码时(暂时保持最后一条 println! 被注释掉),我们将得到以下输出:
When we run this code, keeping the last println! commented out for the
moment, we’ll get this output:
{{#include ../listings/ch15-smart-pointers/listing-15-26/output.txt}}
在我们更改 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。
The reference count of the Rc<List> instances in both a and b is 2 after
we change the list in a to point to b. At the end of main, Rust drops the
variable b, which decreases the reference count of the b Rc<List>
instance from 2 to 1. The memory that Rc<List> has on the heap won’t be
dropped at this point because its reference count is 1, not 0. Then, Rust drops
a, which decreases the reference count of the a Rc<List> instance from 2
to 1 as well. This instance’s memory can’t be dropped either, because the other
Rc<List> instance still refers to it. The memory allocated to the list will
remain uncollected forever. To visualize this reference cycle, we’ve created
the diagram in Figure 15-4.
图 15-4:列表 a 和 b 互相指向的引用循环
如果你取消最后一条 println! 的注释并运行程序,Rust 将尝试打印这个循环: a 指向 b , b 指向 a ,如此循环往复,直到栈溢出。
If you uncomment the last println! and run the program, Rust will try to
print this cycle with a pointing to b pointing to a and so forth until it
overflows the stack.
与实际程序相比,本例中创建引用循环的后果并不是非常严重:在创建引用循环之后,程序立即就结束了。然而,如果一个更复杂的程序在循环中分配了大量内存并持有很长时间,程序将占用比它需要的更多的内存,并可能使系统超负荷,导致可用内存耗尽。
Compared to a real-world program, the consequences of creating a reference cycle in this example aren’t very dire: Right after we create the reference cycle, the program ends. However, if a more complex program allocated lots of memory in a cycle and held onto it for a long time, the program would use more memory than it needed and might overwhelm the system, causing it to run out of available memory.
创建引用循环并不容易,但也并非不可能。如果你拥有包含 Rc<T> 值或类似的内部可变性与引用计数嵌套组合类型的 RefCell<T> 值,你必须确保不创建循环;你不能指望 Rust 来捕获它们。创建引用循环是你程序中的一个逻辑 bug,你应该使用自动化测试、代码审查和其他软件开发实践来将其最小化。
Creating reference cycles is not easily done, but it’s not impossible either.
If you have RefCell<T> values that contain Rc<T> values or similar nested
combinations of types with interior mutability and reference counting, you must
ensure that you don’t create cycles; you can’t rely on Rust to catch them.
Creating a reference cycle would be a logic bug in your program that you should
use automated tests, code reviews, and other software development practices to
minimize.
避免引用循环的另一种解决方案是重构你的数据结构,使某些引用表达所有权,而某些引用则不表达。因此,你可以拥有由某些所有权关系和一些非所有权关系组成的循环,并且只有所有权关系会影响一个值是否可以被丢弃。在示例 15-25 中,我们总是希望 Cons 变体拥有它们的列表,因此重构数据结构是不可行的。让我们看一个由父节点和子节点组成的图示例,看看非所有权关系何时是防止引用循环的一种合适方式。
Another solution for avoiding reference cycles is reorganizing your data
structures so that some references express ownership and some references don’t.
As a result, you can have cycles made up of some ownership relationships and
some non-ownership relationships, and only the ownership relationships affect
whether or not a value can be dropped. In Listing 15-25, we always want Cons
variants to own their list, so reorganizing the data structure isn’t possible.
Let’s look at an example using graphs made up of parent nodes and child nodes
to see when non-ownership relationships are an appropriate way to prevent
reference cycles.
使用 Weak<T> 防止引用循环 (Preventing Reference Cycles Using Weak<T>)
Preventing Reference Cycles Using Weak<T>
到目前为止,我们已经演示了调用 Rc::clone 会增加 Rc<T> 实例的 strong_count (强引用计数),并且只有当其 strong_count 为 0 时 Rc<T> 实例才会被清理。你还可以通过调用 Rc::downgrade 并传递 Rc<T> 的引用,来创建指向 Rc<T> 实例内部值的弱引用。“强引用 (Strong references)” 是你可以共享 Rc<T> 实例所有权的方式。“弱引用 (Weak references)” 不表达所有权关系,它们的计数不会影响 Rc<T> 实例何时被清理。它们不会引起引用循环,因为一旦涉及的值的强引用计数为 0,任何涉及弱引用的循环都会被打破。
So far, we’ve demonstrated that calling Rc::clone increases the
strong_count of an Rc<T> instance, and an Rc<T> instance is only cleaned
up if its strong_count is 0. You can also create a weak reference to the
value within an Rc<T> instance by calling Rc::downgrade and passing a
reference to the Rc<T>. Strong references are how you can share ownership
of an Rc<T> instance. Weak references don’t express an ownership
relationship, and their count doesn’t affect when an Rc<T> instance is
cleaned up. They won’t cause a reference cycle, because any cycle involving
some weak references will be broken once the strong reference count of values
involved is 0.
当你调用 Rc::downgrade 时,你会得到一个 Weak<T> 类型的智能指针。调用 Rc::downgrade 不会将 Rc<T> 实例中的 strong_count 增加 1,而是将 weak_count (弱引用计数)增加 1。 Rc<T> 类型使用 weak_count 来跟踪存在多少个 Weak<T> 引用,类似于 strong_count 。区别在于 weak_count 不需要为 0 就能让 Rc<T> 实例被清理。
When you call Rc::downgrade, you get a smart pointer of type Weak<T>.
Instead of increasing the strong_count in the Rc<T> instance by 1, calling
Rc::downgrade increases the weak_count by 1. The Rc<T> type uses
weak_count to keep track of how many Weak<T> references exist, similar to
strong_count. The difference is the weak_count doesn’t need to be 0 for the
Rc<T> instance to be cleaned up.
因为 Weak<T> 引用的值可能已经被丢弃,要对 Weak<T> 指向的值执行任何操作,你必须确保该值仍然存在。通过在 Weak<T> 实例上调用 upgrade 方法来实现这一点,该方法将返回一个 Option<Rc<T>> 。如果 Rc<T> 值尚未被丢弃,你将得到 Some 结果;如果 Rc<T> 值已被丢弃,你将得到 None 结果。因为 upgrade 返回 Option<Rc<T>> ,Rust 将确保 Some 情况和 None 情况都得到处理,并且不会出现无效指针。
Because the value that Weak<T> references might have been dropped, to do
anything with the value that a Weak<T> is pointing to you must make sure the
value still exists. Do this by calling the upgrade method on a Weak<T>
instance, which will return an Option<Rc<T>>. You’ll get a result of Some
if the Rc<T> value has not been dropped yet and a result of None if the
Rc<T> value has been dropped. Because upgrade returns an Option<Rc<T>>,
Rust will ensure that the Some case and the None case are handled, and
there won’t be an invalid pointer.
举个例子,我们不再使用其项只知道下一个项的列表,而是创建一个其项知道其子项“以及”其父项的树。
As an example, rather than using a list whose items know only about the next item, we’ll create a tree whose items know about their child items and their parent items.
创建树形数据结构 (Creating a Tree Data Structure)
Creating a Tree Data Structure
首先,我们将构建一棵带有知道其子节点节点的树。我们将创建一个名为 Node 的结构体,它持有其自身的 i32 值以及对其子 Node 值的引用:
To start, we’ll build a tree with nodes that know about their child nodes.
We’ll create a struct named Node that holds its own i32 value as well as
references to its child Node values:
文件名: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-27/src/main.rs:here}}
}
我们希望一个 Node 拥有其子节点,并且我们希望与变量共享该所有权,以便我们可以直接访问树中的每个 Node 。为此,我们将 Vec<T> 项定义为 Rc<Node> 类型的值。我们还想修改哪些节点是另一个节点的子节点,所以我们在 children 的 Vec<Rc<Node>> 周围放了一个 RefCell<T> 。
We want a Node to own its children, and we want to share that ownership with
variables so that we can access each Node in the tree directly. To do this,
we define the Vec<T> items to be values of type Rc<Node>. We also want to
modify which nodes are children of another node, so we have a RefCell<T> in
children around the Vec<Rc<Node>>.
接下来,我们将使用我们的结构体定义并创建一个名为 leaf 的 Node 实例,其值为 3 且没有子节点,以及另一个名为 branch 的实例,其值为 5 并且将 leaf 作为其子节点之一,如示例 15-27 所示。
Next, we’ll use our struct definition and create one Node instance named
leaf with the value 3 and no children, and another instance named branch
with the value 5 and leaf as one of its children, as shown in Listing 15-27.
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-27/src/main.rs:there}}
}
我们克隆了 leaf 中的 Rc<Node> 并将其存储在 branch 中,这意味着 leaf 中的 Node 现在有两个所有者: leaf 和 branch 。我们可以通过 branch.children 从 branch 到达 leaf ,但没有办法从 leaf 到达 branch 。原因是 leaf 没有 branch 的引用,也不知道它们是相关的。我们希望 leaf 知道 branch 是它的父节点。我们接下来就做这件事。
We clone the Rc<Node> in leaf and store that in branch, meaning the
Node in leaf now has two owners: leaf and branch. We can get from
branch to leaf through branch.children, but there’s no way to get from
leaf to branch. The reason is that leaf has no reference to branch and
doesn’t know they’re related. We want leaf to know that branch is its
parent. We’ll do that next.
添加从子节点到其父节点的引用 (Adding a Reference from a Child to Its Parent)
为了让子节点意识到其父节点,我们需要在 Node 结构体定义中添加一个 parent 字段。困难在于决定 parent 的类型应该是什么。我们知道它不能包含 Rc<T> ,因为那会形成引用循环,即 leaf.parent 指向 branch 而 branch.children 指向 leaf ,这将导致它们的 strong_count 值永远不会为 0。
To make the child node aware of its parent, we need to add a parent field to
our Node struct definition. The trouble is in deciding what the type of
parent should be. We know it can’t contain an Rc<T>, because that would
create a reference cycle with leaf.parent pointing to branch and
branch.children pointing to leaf, which would cause their strong_count
values to never be 0.
换种方式思考这些关系,父节点应该拥有其子节点:如果父节点被丢弃,其子节点也应该被丢弃。然而,子节点不应该拥有其父节点:如果我们丢弃子节点,父节点应该仍然存在。这是一个弱引用的应用场景!
Thinking about the relationships another way, a parent node should own its children: If a parent node is dropped, its child nodes should be dropped as well. However, a child should not own its parent: If we drop a child node, the parent should still exist. This is a case for weak references!
所以,我们不使用 Rc<T> ,而是让 parent 的类型使用 Weak<T> ,具体来说是 RefCell<Weak<Node>> 。现在我们的 Node 结构体定义看起来像这样:
So, instead of Rc<T>, we’ll make the type of parent use Weak<T>,
specifically a RefCell<Weak<Node>>. Now our Node struct definition looks
like this:
文件名: src/main.rs
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-28/src/main.rs:here}}
}
一个节点将能够引用其父节点,但不拥有其父节点。在示例 15-28 中,我们更新 main 以使用这个新定义,以便 leaf 节点能够引用其父节点 branch 。
A node will be able to refer to its parent node but doesn’t own its parent. In
Listing 15-28, we update main to use this new definition so that the leaf
node will have a way to refer to its parent, branch.
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-28/src/main.rs:there}}
}
创建 leaf 节点看起来与示例 15-27 类似,除了 parent 字段: leaf 一开始没有父节点,所以我们创建了一个新的空 Weak<Node> 引用实例。
Creating the leaf node looks similar to Listing 15-27 with the exception of
the parent field: leaf starts out without a parent, so we create a new,
empty Weak<Node> reference instance.
此时,当我们尝试通过使用 upgrade 方法获取 leaf 的父节点引用时,我们得到了一个 None 值。我们在第一个 println! 语句的输出中看到了这一点:
At this point, when we try to get a reference to the parent of leaf by using
the upgrade method, we get a None value. We see this in the output from the
first println! statement:
leaf parent = None
当我们创建 branch 节点时,它在 parent 字段中也将有一个新的 Weak<Node> 引用,因为 branch 没有父节点。我们仍然将 leaf 作为 branch 的子节点之一。一旦我们在 branch 中有了 Node 实例,我们就可以修改 leaf ,给它一个指向其父节点的 Weak<Node> 引用。我们在 leaf 的 parent 字段中的 RefCell<Weak<Node>> 上使用 borrow_mut 方法,然后使用 Rc::downgrade 函数从 branch 中的 Rc<Node> 创建一个指向 branch 的 Weak<Node> 引用。
When we create the branch node, it will also have a new Weak<Node>
reference in the parent field because branch doesn’t have a parent node. We
still have leaf as one of the children of branch. Once we have the Node
instance in branch, we can modify leaf to give it a Weak<Node> reference
to its parent. We use the borrow_mut method on the RefCell<Weak<Node>> in
the parent field of leaf, and then we use the Rc::downgrade function to
create a Weak<Node> reference to branch from the Rc<Node> in branch.
当我们再次打印 leaf 的父节点时,这次我们将得到一个持有 branch 的 Some 变体:现在 leaf 可以访问它的父节点了!当我们打印 leaf 时,我们也避免了像示例 15-26 中那样最终以栈溢出告终的循环; Weak<Node> 引用被打印为 (Weak) :
When we print the parent of leaf again, this time we’ll get a Some variant
holding branch: Now leaf can access its parent! When we print leaf, we
also avoid the cycle that eventually ended in a stack overflow like we had in
Listing 15-26; the Weak<Node> references are printed as (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 得到的值来判断这一点。
The lack of infinite output indicates that this code didn’t create a reference
cycle. We can also tell this by looking at the values we get from calling
Rc::strong_count and Rc::weak_count.
可视化 strong_count 和 weak_count 的变化 (Visualizing Changes to strong_count and weak_count)
Visualizing Changes to strong_count and weak_count
让我们看看通过创建一个新的内部作用域并将 branch 的创建移动到该作用域中, Rc<Node> 实例的 strong_count 和 weak_count 值是如何变化的。通过这样做,我们可以看到当 branch 被创建以及随后在它超出作用域时被丢弃时会发生什么。修改后的代码如示例 15-29 所示。
Let’s look at how the strong_count and weak_count values of the Rc<Node>
instances change by creating a new inner scope and moving the creation of
branch into that scope. By doing so, we can see what happens when branch is
created and then dropped when it goes out of scope. The modifications are shown
in Listing 15-29.
#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-29/src/main.rs:here}}
}
创建 leaf 后,其 Rc<Node> 具有强计数 1 和弱计数 0。在内部作用域中,我们创建 branch 并将其与 leaf 关联,此时当我们打印计数时, branch 中的 Rc<Node> 将具有强计数 1 和弱计数 1(对应 leaf.parent 通过 Weak<Node> 指向 branch )。当我们打印 leaf 中的计数时,我们会看到它将具有强计数 2,因为 branch 现在在 branch.children 中存储了 leaf 的 Rc<Node> 的克隆,但它仍将具有弱计数 0。
After leaf is created, its Rc<Node> has a strong count of 1 and a weak
count of 0. In the inner scope, we create branch and associate it with
leaf, at which point when we print the counts, the Rc<Node> in branch
will have a strong count of 1 and a weak count of 1 (for leaf.parent pointing
to branch with a Weak<Node>). When we print the counts in leaf, we’ll see
it will have a strong count of 2 because branch now has a clone of the
Rc<Node> of leaf stored in branch.children but will still have a weak
count of 0.
当内部作用域结束时, branch 超出作用域, Rc<Node> 的强计数减少到 0,因此其 Node 被丢弃。来自 leaf.parent 的 1 个弱计数与 Node 是否被丢弃无关,因此我们没有得到任何内存泄漏!
When the inner scope ends, branch goes out of scope and the strong count of
the Rc<Node> decreases to 0, so its Node is dropped. The weak count of 1
from leaf.parent has no bearing on whether or not Node is dropped, so we
don’t get any memory leaks!
如果我们尝试在作用域结束后访问 leaf 的父节点,我们将再次得到 None 。在程序结束时, leaf 中的 Rc<Node> 具有强计数 1 和弱计数 0,因为变量 leaf 现在又是 Rc<Node> 的唯一引用了。
If we try to access the parent of leaf after the end of the scope, we’ll get
None again. At the end of the program, the Rc<Node> in leaf has a strong
count of 1 and a weak count of 0 because the variable leaf is now the only
reference to the Rc<Node> again.
所有管理计数和值丢弃的逻辑都内置在 Rc<T> 和 Weak<T> 及其 Drop 特征的实现中。通过在 Node 的定义中指定从子节点到其父节点的关系应该是 Weak<T> 引用,你就能够让父节点指向子节点,反之亦然,而不会创建引用循环和内存泄漏。
All of the logic that manages the counts and value dropping is built into
Rc<T> and Weak<T> and their implementations of the Drop trait. By
specifying that the relationship from a child to its parent should be a
Weak<T> reference in the definition of Node, you’re able to have parent
nodes point to child nodes and vice versa without creating a reference cycle
and memory leaks.
总结 (Summary)
本章介绍了如何使用智能指针来做出与 Rust 对普通引用的默认保证不同的保证和权衡。 Box<T> 类型具有已知的大小并指向分配在堆上的数据。 Rc<T> 类型跟踪堆上数据的引用数量,以便数据可以拥有多个所有者。具有内部可变性的 RefCell<T> 类型为我们提供了一种类型,当我们既需要一个不可变类型但又需要更改该类型的内部值时可以使用它;它还在运行时而非编译时强制执行借用规则。
This chapter covered how to use smart pointers to make different guarantees and
trade-offs from those Rust makes by default with regular references. The
Box<T> type has a known size and points to data allocated on the heap. The
Rc<T> type keeps track of the number of references to data on the heap so
that the data can have multiple owners. The RefCell<T> type with its interior
mutability gives us a type that we can use when we need an immutable type but
need to change an inner value of that type; it also enforces the borrowing
rules at runtime instead of at compile time.
还讨论了 Deref 和 Drop 特征,它们实现了智能指针的大部分功能。我们探索了可能导致内存泄漏的引用循环,以及如何使用 Weak<T> 来防止它们。
Also discussed were the Deref and Drop traits, which enable a lot of the
functionality of smart pointers. We explored reference cycles that can cause
memory leaks and how to prevent them using Weak<T>.
如果本章引起了你的兴趣,并且你想实现自己的智能指针,请查看 “The Rustonomicon” 以获取更多有用信息。
If this chapter has piqued your interest and you want to implement your own smart pointers, check out “The Rustonomicon” for more useful information.
接下来,我们将讨论 Rust 中的并发。你甚至会了解到一些新的智能指针。
Next, we’ll talk about concurrency in Rust. You’ll even learn about a few new smart pointers.