参考周期可能会泄漏内存

Rust 的内存安全保证使其难以(但并非不可能) 意外创建从未清理的内存(称为内存泄漏)。 完全防止内存泄漏并不是 Rust 的保证之一,这意味着 内存泄漏在 Rust 中是内存安全的。我们可以看到 Rust 允许内存泄漏 通过使用Rc<T>RefCell<T>:可以在其中 项在一个循环中相互引用。这会产生内存泄漏,因为 循环中每个项目的引用计数永远不会达到 0,并且值 永远不会被丢弃。

创建引用循环

让我们看看引用循环是如何发生的以及如何防止它, 从Listenum 和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>所以我们可以修改Consvariant 指的是

我们使用的是List定义。这 second 元素中的Consvariant 现在是RefCell<Rc<List>>,这意味着 而不是能够修改i32value 就像我们在 Listing 中所做的那样 15-24 中,我们要修改List值 AConsvariant 指向。 我们还添加了一个tail方法,以便我们访问 second item(如果我们有Cons变体。

在示例 15-26 中,我们添加了一个main函数,该函数使用 示例 15-25.此代码在ab这指向 列表中的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>实例持有Listvalue 在变量中a初始列表为5, Nil.然后,我们创建一个Rc<List>实例持有 另一个Listvalue 在变量中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>实例ab在 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:列表的参考循环ab彼此指向

如果取消注释最后一个println!并运行该程序,Rust 将尝试 打印此循环a指向b指向a依此类推,直到它 溢出堆栈。

与实际程序相比,创建参考循环的后果 在这个例子中不是很可怕:在我们创建引用循环之后, 程序结束。但是,如果更复杂的程序分配了大量内存 在一个循环中并长时间保持它,程序将使用更多的内存 超过它需要的,并且可能会使系统不堪重负,导致它耗尽 可用内存。

创建参考循环并不容易,但也并非不可能。 如果你有RefCell<T>包含Rc<T>值或类似的嵌套 类型与内部可变性和引用计数的组合,您必须 确保你不创建周期;你不能指望 Rust 来捕捉它们。 创建引用循环将是程序中的一个逻辑错误,您应该这样做 使用自动化测试、代码审查和其他软件开发实践来 最小化。

避免引用循环的另一种解决方案是重新组织数据 结构,以便某些引用表示所有权,而某些引用则不表示所有权。 因此,您可以拥有由一些所有权关系和 某些非所有权关系,并且只有所有权关系会影响 是否可以删除值。在示例 15-25 中,我们总是希望Consvariants 来拥有其列表,因此无法重新组织数据结构。 让我们看一个使用由父节点和子节点组成的图形的示例 查看何时使用非所有权关系是防止 引用循环。

防止参考循环:转动Rc<T>转换为Weak<T>

到目前为止,我们已经证明了调用Rc::clone增加strong_countRc<T>实例和Rc<T>仅清理实例 如果它是strong_count为 0。您还可以创建对 值在Rc<T>实例Rc::downgrade并传递一个 对Rc<T>.强引用是您共享 一Rc<T>实例。弱引用不表达所有权关系, 并且它们的计数不会影响Rc<T>实例被清理。他们 不会导致引用循环,因为任何涉及一些弱引用的循环 一旦涉及的值的强引用计数为 0,就会被中断。

当您调用Rc::downgrade中,您将获得Weak<T>. 而不是增加strong_countRc<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 将确保Somecase 和Nonecase 的 不会有无效的指针。

例如,而不是使用其项只知道下一个 item 中,我们将创建一个树,其 items 了解其子 items 父 items。

创建树数据结构:一个Node与子节点

首先,我们将构建一个包含知道其子节点的节点的树。 我们将创建一个名为Node这有自己的i32value 以及 对其子项的引用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没有子节点的节点 以及branchnode 替换为leaf作为其子项之一

我们克隆Rc<Node>leaf并将其存储在branch,表示Nodeleaf现在有两个所有者:leafbranch.我们可以从branchleaf通过branch.children,但是没有办法从leafbranch.原因是leaf没有引用branch和 不知道他们是有关系的。我们希望leaf要知道branch是其 父母。我们接下来会这样做。

添加从 Child 到 Parent 的引用

要使子节点知道其父节点,我们需要添加一个parentfield 设置为 我们Nodestruct 定义。问题在于决定parent应该是。我们知道它不能包含Rc<T>,因为那样 创建参考循环leaf.parent指向branchbranch.children指向leaf,这会导致其strong_count值永远不会为 0。

从另一个角度考虑关系,父节点应该拥有其 children:如果删除了父节点,则应将其子节点删除为 井。但是,子节点不应拥有其父节点:如果我们删除子节点,则 parent 应该仍然存在。这是弱引用的情况!

所以而不是Rc<T>,我们将 type 为parentWeak<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来使用这个新定义,以便leafnode 将有一种方式来引用它的父级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

创建leafnode 看起来类似于示例 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_countRc::weak_count.

可视化对strong_countweak_count

让我们看看strong_countweak_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:创建branchin 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>类型及其内部 可变性为我们提供了一个类型,当我们需要一个不可变类型时,我们可以使用它,但是 需要更改该类型的内部值;它还强制执行借用 规则。

还讨论了DerefDroptrait 中实现的许多 智能指针的功能。我们探讨了可能导致 内存泄漏以及如何使用Weak<T>.

如果本章激起了您的兴趣,并且您想实施自己的 智能指针,请查看 “The Rustonomicon” 了解更多有用 信息。

接下来,我们将讨论 Rust 中的并发性。您甚至会了解一些新的 智能指针。

本文档由官方文档翻译而来,如有差异请以官方英文文档(https://doc.rust-lang.org/)为准