You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
rust_basic_code/md_file/15 智能指针.md

57 KiB

智能指针

指针是一个通用概念,指的是一个包含内存地址的变量,这个地址指向或者“指向”其他数据。在 Rust 中,最常见的指针是引用(reference),它使用 & 符号表示,并且借用它所指向的值。引用没有任何特殊的功能,除了指向数据之外,并且没有开销。

智能指针是数据结构,它的行为类似于指针,但也具有附加的元数据和功能。智能指针的概念并不局限于 Rust:智能指针起源于 C++,并且在其他语言中也存在。Rust 在标准库中定义了各种智能指针,提供了比引用更多的功能。为了探讨智能指针的一般概念,我们将看几个不同的智能指针的例子,包括一个引用计数智能指针类型。这个指针可以通过跟踪拥有者的数量来允许数据有多个所有者,并在没有所有者时清理数据。

在 Rust 中,引用和智能指针之间有一个额外的区别,这是由于 Rust 的所有权和借用概念所带来的:引用只借用数据,在许多情况下,智能指针拥有它们所指向的数据

StringVec,这两种类型都被认为是智能指针,因为它们拥有一些数据并允许您操作它。它们还具有元数据(容量)和额外的功能或保证。例如,String 将其容量存储为元数据,并具有确保其数据始终为有效的 UTF-8 的额外功能。

智能指针通常使用结构体实现。与普通结构体不同,智能指针实现了 DerefDrop trait。Deref trait允许智能指针结构的实例像引用一样工作,这样就可以编写既用于引用又用于智能指针的代码。Drop 特征允许您自定义在智能指针实例超出作用域时运行的代码

智能指针模式是 Rust 中经常使用的一种通用设计模式。可自己编写自己的智能指针,标准库中常用的智能指针有:

  • Box 用于在堆上分配值
  • Rc,一种引用计数类型,其数据允许多个所有权
  • Ref 和 RefMut,通过 RefCell 访问,一个在运行时而不是在编译时执行借用规则的类型。

另外会涉及 内部可变性(interior mutability)模式,这时不可变类型暴露出改变其内部值的 API。我们也会讨论 引用循环(reference cycles)会如何泄露内存,以及如何避免。

Box 指向堆上数据

最简单的智能指针是Box,其类型被写作Box。Box允许你将一个值放在堆上而不是栈上。在栈上仅剩下指向堆数据的指针。除了将数据存储在堆上而不是栈上之外,Box没有性能开销,但它们也没有太多额外的功能。它们多用于如下场景:

  1. 当有一个在编译时未知大小的类型,而又想要在需要确切大小的上下文中使用这个类型值的时候
  2. 当有大量数据并希望在确保数据不被拷贝的情况下转移所有权的时候
  3. 当希望拥有一个值并只关心它的类型是否实现了特定 trait 而不是其具体类型的时候

使用 Box 在堆上存储数据

fn main() {
    let b = Box::new(5);
    println!("b = {}", b);
}

变量b值是一个指向被分配在上值 5 的Box。这个程序会打印出 b = 5;在这个例子中,我们可以像数据是储存在栈上的那样访问 box 中的数据。正如任何拥有数据所有权的值那样,当像 b 这样的 box 在 main 的末尾离开作用域时,它将被释放。这个释放过程作用于 box 本身(位于栈上)和它所指向的数据(位于堆上)。

将一个单独的值存放在堆上并不是很有意义,这样单独使用 box 并不常见。将像单个 i32 这样的值储存在栈上,也就是其默认存放的地方在大部分使用场景中更为合适。下面是不使用 box 时无法定义的类型的例子

Box允许创建递归类型

递归类型的值可以是相同类型的另一个值。递归类型存在一个问题,因为在编译时Rust需要知道一个类型占用多少空间。这种值的嵌套理论上可以无限的进行下去,所以 Rust 不知道递归类型需要多少空间。由于Box有已知的大小,所以通过在循环类型定义中插入 box,就可以创建递归类型了。

cons list 介绍

cons list 是函数式编程语言的数据类型。通常被用作链表的实现方式。由嵌套的 pair 组成,是链表的 Lisp 版本。它的名称来自于 Lisp 中的 cons 函数(缩写为“构造函数”),该函数从其两个参数构造一个新的 pair。通过在由值和另一个 pair 组成的 pair 上调用 cons,我们可以构造由递归 pair 组成的 cons list。

例如,下面是包含列表 1、2、3 的 cons list 的伪代码表示形式,其中每个 pair 用括号括起来:

(1, (2, (3, Nil)))

cons list 中的每个项都包含两个元素:当前项的值和下一个项。列表中的最后一项仅包含一个名为 Nil 的值,而没有下一个项。通过递归调用 cons 函数来生成 cons list。代表递归的终止条件(base case)的规范名称是 Nil。Nil与“null”或“nil”概念不同,后者是无效或不存在的值。

注意虽然函数式编程语言经常使用 cons list,但是它并不是一个 Rust 中常见的类型。大部分在 Rust 中需要列表的时候,Vec 是一个更好的选择。其他更为复杂的递归数据类型 确实 在 Rust 的很多场景中很有用,不过通过以 cons list 作为开始,我们可以探索如何使用 box 毫不费力的定义一个递归数据类型。

cons list枚举定义,目前实现一个只存放 i32 值的 cons list(可以用泛型):

enum List {
    Cons(i32, List),
    Nil,
}
// 当List类型有大小时,才可以编译, 

使用此cons list进行存储:

image-20230426145322238

第一个 Cons 储存了 1 和另一个 List 值。这个 List 是另一个包含 2 的 Cons 值和下一个 List 值。接着又有另一个存放了 3 的 Cons 值和最后一个值为 Nil 的 List,非递归成员代表了列表的结尾。

error[E0072]: recursive type List has infinite size 表明这个类型“具有无限大小”。其原因是 List 的一个成员被定义为是递归的:它直接存放了另一个相同类型的值。这意味着 Rust 无法计算为了存放 List 值到底需要多少空间。

计算非递归类型的大小

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

rust检查其每个成员,为此Message枚举类型应分配的空间。Message枚举的空间等于储存其最大成员的空间大小。使用size_of检查类型大小:

image-20230426154221060

由此可知 List 枚举(Cons 变体)产生“具有无限大小”错误的原因。编译器首先查看 Cons 变体,该变体包含一个类型为 i32 的值和一个类型为 List 的值。因此,Cons 需要的空间量等于 i32 大小加上 List 大小。为了确定 List 类型需要多少内存,编译器查看变体,从 Cons 变体开始。Cons 变体包含一个类型为 i32 的值和一个类型为 List 的值,这个过程无限继续下去。 简单来说就是在算最大成员的空间时,进入了递归

trpl15-01

使用Box 给递归类型一个已知的大小

可观察编译器的提示进行修改:

image-20230426160009954

 help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
 // 插入一些间接引用(例如Box、Rc或&),以打破循环。

“indirection”意味着,我们应该改变数据结构,通过存储指向值的指针来间接地存储值,而不是直接存储值。指针的大小不会根据它指向的数据量而改变。故可以将Box放在Cons变体中,而不是直接放置另一个List值。Box会指向另一个位于堆上的 List 值,而不是存放在 Cons 成员中。

从概念上讲,我们仍然有一个通过在其中存放其他列表创建的列表,不过现在实现这个概念的方式更像是一个项挨着另一项,而不是一项包含另一项。

use List::{Cons, Nil};

enum List {
    Cons(i32, Box<List>),
    Nil,
}

fn main(){
    let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}

image-20230426171511681

通过使用 Box,结束了计算空间的递归。目前Cons 需要一个 i32 的大小加上储存 box 指针数据的空间。Nil 成员不储存值,所以它比 Cons 成员需要更少的空间。故目前储存List 值的空间可以计算出。图示:

trpl15-02

Box 类型是一个智能指针,因为它实现了 Deref trait,它允许 Box 值被当作引用对待。当 Box 值离开作用域时,由于 Box 类型 Drop trait 的实现,box 所指向的堆数据也会被清除。

Deref Trait 将智能指针当作常规引用处理

实现 Deref trait允许我们自定义解引用运算符 * 的行为,可编写既用于引用又用于智能指针的代码。

指针解引用

Reference是一种安全的指针,引用提供了对值的访问,同时也遵循了 Rust 的所有权和借用规则,确保了内存安全和线程安全。

fn main() {
    let x = 5;
    let y = &x;

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

若不解引用直接将y与5比较:

image-20230427095119372

error[E0277]: can't compare `{integer}` with `&{integer}`
 --> src\main.rs:7:5
  |
7 |     assert_eq!(5, y);
  |     ^^^^^^^^^^^^^^^^ no implementation for `{integer} == &{integer}`
  |
  = help: the trait `PartialEq<&{integer}>` is not implemented for `{integer}`
  = help: the following other types implement trait `PartialEq<Rhs>`:
            f32
            f64
            i128
            i16
            i32
            i64
            i8
            isize
          and 6 others
  = note: this error originates in the macro `assert_eq` (in Nightly builds, run with -Z macro-backtrace for more info)

For more information about this error, try `rustc --explain E0277`.
error: could not compile `point_test` due to previous error

Process finished with exit code 101

使用Box替代reference

智能指针结构的实例像引用一样工作:

fn main() {
    let x = 5;
    let y = Box::new(x);
    assert_eq!(5, x);
    assert_eq!(5, *y);
}

同样Box若不解引用直接将y与5比较:

image-20230427104219959

error[E0277]: can't compare `{integer}` with `Box<{integer}>`
 --> src\main.rs:5:5
  |
5 |     assert_eq!(5, y);
  |     ^^^^^^^^^^^^^^^^ no implementation for `{integer} == Box<{integer}>`
  |
  = help: the trait `PartialEq<Box<{integer}>>` is not implemented for `{integer}`
  = help: the following other types implement trait `PartialEq<Rhs>`:
            f32
            f64
            i128
            i16
            i32
            i64
            i8
            isize
          and 6 others
  = note: this error originates in the macro `assert_eq` (in Nightly builds, run with -Z macro-backtrace for more info)

For more information about this error, try `rustc --explain E0277`.
error: could not compile `point_test` due to previous error

自定义“智能指针”

首先仿照 Box 类型,创建一个类似的结构体 MyBox,并添加 new()。然后使用 MyBox 替换标准库中的 Box。

image-20230427110732419

error[E0614]: type `MyBox<{integer}>` cannot be dereferenced
  --> src\main.rs:14:19
   |
14 |     assert_eq!(5, *y);
   |                   ^^

For more information about this error, try `rustc --explain E0614`.
error: could not compile `my_box` due to previous error

注:MyBox 实际上并没有在堆上分配内存。它只是一个包装(wrapper)类型,包装了类型为 T 的值。在 Rust 中,包装类型是通过组合(composition)来实现的,即将其他类型包含在自定义类型中。因此,MyBox 内部的值将会被直接存储在调用 MyBox::new(x) 时的帧(stack frame)中,而不是在堆上。

Box 是一种智能指针(smart pointer),它的作用是将值分配在堆上,并提供对这个值的所有权。它的实现方式是通过使用 malloc 或类似函数在堆上分配一块内存来存储值,然后将这块内存的地址保存在指向 Box 的指针中。由于指针本身是在栈上分配的,因此它的大小是固定的,而保存在堆上的值的大小则可以根据需要动态变化。

为“智能指针”添加所需trait

DerefDrop trait是智能指针区别于常规结构体的显著特性。MyBox 没有实现智能指针常用的 DerefDrop trait,因此严格来说,它不能被认为是一个智能指针类型。Deref trait文档

pub trait Deref {
    type Target: ?Sized;

    // Required method
    fn deref(&self) -> &Self::Target;
}

使 MyBox 实现 Deref trait

use std::ops::Deref;

struct MyBox<T>(T);

impl <T> MyBox<T> {
    fn new(x: T) -> MyBox<T>{
        MyBox(x)
    }
}

impl <T> Deref for MyBox<T>{
    type Target = T;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

fn main() {
    let x = 5;
    let y = MyBox::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

image-20230427143758414

type Target = T; 语法定义了 Deref trait 的关联类型。关联类型是声明泛型参数的一种稍微不同的方式。关联类型(associated type)是 Rust 中一种用于声明 trait 中类型别名的机制,它允许 trait 定义中使用的类型与实现该 trait 的具体类型关联起来。通过关联类型,trait 可以声明一个或多个类型占位符,然后在具体类型实现该 trait 时指定具体的类型。

编译器只能对 & 引用进行解引用操作,而不能对其他类型的引用进行解引用操作Rust通过 Deref trait 解决了这个问题Deref trait 是一个可以将一个类型的实例转换为另一个类型的实例的 trait,它提供了一种方式来自定义解引用操作。通过实现 Deref trait,我们可以告诉编译器如何将我们自己的类型转换为 & 引用,并使编译器能够在必要时自动调用我们自己的 deref 方法来完成类型转换并进行解引用操作。

在本例中,MyBox 类型实现了 Deref trait,因此当我们使用 *y 进行解引用时,Rust 会自动调用 deref 方法,即 y.deref(),以获取一个指向 MyBox 中包含的内部值的引用。然后 * 运算符会被应用于这个引用,从而使我们能够使用 *y 来访问 MyBox 中的值,就好像它是一个常规的指针一样。在幕后,Rust实际上运行了这段代码:

*(y.deref());

Rust 将 * 运算符替换为先调用 deref 方法再进行解引用的操作,如此我们便不用担心是不是还需要手动调用 deref 方法了。Rust 的这个特性可以让我们写出行为一致的代码,无论是面对的是常规引用还是实现了 Deref 的类型。

deref 方法返回的是值的引用,而不是值本身,这是为了避免移动所有权。如果返回值本身,则会移动值的所有权出 MyBox,这并不是我们希望的,因为我们想保持对 MyBox 的所有权。为了获取值的实际内容,我们需要在其引用上使用解引用运算符,也就是 *(y.deref()),这样就可以获取值的引用所指向的内容。

image-20230427154750184

函数和方法的隐式解引用强制转换(Deref coercion)

Deref coercion 是 Rust 语言的一个特性,它可以将实现了 Deref trait 的类型的引用自动转换成指向其他类型的引用。例如,&String 可以被自动转换为 &str,因为 String 实现了 Deref trait 并返回 &str。这个特性只适用于实现了 Deref trait 的类型,并且只在函数和方法的参数中发生。当我们将一个特定类型的值的引用作为参数传递给函数或方法,而这个参数的类型与函数或方法的定义不匹配时,这个特性会自动触发。通过一系列的 deref 方法调用,我们提供的类型将被转换成函数或方法所需的类型。

Rust 添加了 Deref coercion 这个特性是为了让开发者在编写函数和方法调用时不需要频繁地使用 & 和 * 进行显式的引用和解引用操作。这个特性还可以让我们编写更通用的代码,既适用于引用又适用于智能指针String 文档

#[stable(feature = "rust1", since = "1.0.0")]
impl ops::Deref for String {
    type Target = str;

    #[inline]
    fn deref(&self) -> &str {
        unsafe { str::from_utf8_unchecked(&self.vec) }
    }
}

String 实现了 Deref<Target = str>,因此继承了 str 的所有方法。此外,这意味着可以通过使用 &,将 String 传递给一个需要 &str 的函数:

fn takes_str(s: &str) { }

let s = String::from("Hello");

takes_str(&s);

使用 MyBox 检验 deref coercion 特征

use std::ops::Deref;

impl <T> Deref for MyBox<T>{
    type Target = T;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

struct MyBox<T>(T);

impl <T> MyBox<T> {
    fn new(x: T) -> MyBox<T>{
        MyBox(x)
    }
}
fn hello(name: &str){
    println!("hello, {}", name);
}

fn main(){
    let m = MyBox::new(String::from("zhu"));
    hello(&m);
    hello(&(*m)[0..2]);
    hello(&(*(m.deref()))[..]);
    hello(&(**(m.deref()))[..]);
    hello(&(***(m.deref()))[..]);
}

image-20230427162228011

在这里,我们使用参数 &m 调用了 hello 函数,&m 是一个指向 MyBox 值的引用。因为我们在 Listing 15-10 中实现了 MyBox 上的 Deref trait,所以 Rust 可以通过调用 deref 将 &MyBox 转换为 &String。标准库在 String 上提供了一个 Deref 实现,返回一个字符串切片,Rust 再次调用 deref 将 &String 转换为 &str,这与 hello 函数的定义匹配。

在 Rust 中,deref 方法是用于将一个值从实现了 Deref trait 的类型转换成其 Target 类型的引用。在这种情况下,&MyBox<String> 是一个指向 MyBox<String> 的引用,而 MyBox<String> 实现了 Deref trait,使其能够被转换为 String 的引用。因此,当 Rust 看到 &MyBox<String> 作为参数传递给一个函数或方法时,它会自动调用 deref 方法,将其转换为 &String,再次调用 deref 方法,将其转换为 &str,这样就匹配了函数或方法的参数类型。所以,虽然 & 符号通常表示取引用,但在这种情况下,它实际上是表示对引用进行了 Deref 操作。

image-20230427170419589

(*m)  `MyBox<String>` 解引用为 String, 
(**(m.deref()) 将`MyBox<String> 解引用为 str

当涉及到的类型定义了 Deref trait 时,Rust 编译器会分析变量的类型并自动进行解引用强制转换。也就是说,如果传递给函数的参数类型与函数定义中的参数类型不匹配,Rust 编译器会尝试自动应用 Deref trait 来进行类型转换。这样做的过程就是不断调用 Deref::deref 函数,将变量的层级引用一层层地转换为与参数类型匹配的引用类型。需要插入 Deref::deref 的次数是在编译时确定的,所以使用解引用强制转换不会产生额外的运行时开销。

解引用强制转换(Deref Coercion)与可变性(Mutability)的交互

类似于如何使用 Deref trait 重载不可变引用的 * 运算符,Rust 提供了 DerefMut trait 用于重载可变引用的 * 运算符。

Rust 在发现类型和 trait 实现满足三种情况时会进行解引用强制转换(多态):

当 T: Deref<Target=U> 时从 &T 到 &U。 当 T: DerefMut<Target=U> 时从 &mut T 到 &mut U。 当 T: Deref<Target=U> 时从 &mut T 到 &U。

前两种情况是相同的,只是第二种实现了可变性。第一种情况表示,第一种情况表明如果有一个 &T,而 T 实现了返回 U 类型的 Deref,则可以直接得到 &U。第二种情况表明对于可变引用也有着相同的行为。

第三种情况比较特征:Rust 也会将可变引用强制转换为不可变引用。但反过来是不可能的:不可变引用永远不会转换为可变引用。由于借用规则,如果你有一个可变引用,那么这个可变引用必须是该数据的唯一引用(否则程序将无法编译)。将一个可变引用转换为一个不可变引用永远不会违反借用规则。将一个不可变引用转换为一个可变引用需要保证只能有一个不可变引用,但借用规则并不保证这一点。因此,Rust 不能假定将不可变引用转换为可变引用是可行的。

使用 Drop Trait 运行清理代码

DerefDrop trait是智能指针区别于常规结构体的显著特性。智能指针模式中的第二个重要 trait 是 Drop,它允许我们自定义当值要离开作用域时执行一些代码。Drop trait 可以在任何类型上实现,这些代码可用于释放诸如文件或网络连接之类的资源,而无需程序员手动释放资源。例如,当 Box 被 dropped 时,它将释放堆上指向的空间。

在 Rust 中,可以指定一些代码应该在值离开作用域时被执行,而编译器会自动插入这些代码。于是我们就不需要在程序中到处编写在实例结束时清理这些变量的代码 —— 而且还不会泄露资源。通过实现 Drop trait 来指定在值超出作用域时运行的代码。Drop trait 要求您实现一个名为 drop 的方法,该方法接受一个对 self 的可变引用。

pub trait Drop {
    // Required method
    fn drop(&mut self);
}

测试代码:

struct CustomSmartPointer{
    data: String,
}

impl Drop for CustomSmartPointer{
    fn drop(&mut self){
        println!("Dropping CustomSmartPointer with data `{}`!", self.data);
    }
}

fn main() {
    let c =  CustomSmartPointer{
        data: String::from("my stuff"),
    };
    let d = CustomSmartPointer{
        data: String::from("other stuff"),
    };
    println!("CustomSmartPointers created.");
}

运行图:

image-20230504111925411

Drop trait 包含在 prelude(prelude 是指 Rust 编译器自动导入到每个模块中的一组常见类型和 trait 的模块) 中,所以无需导入它。我们在 CustomSmartPointer 上实现了 Drop trait,并提供了一个调用 println! 的 drop 方法实现。drop 函数体是放置任何当类型实例离开作用域时期望运行的逻辑的地方。这里选择打印一些文本以展示 Rust 何时调用 drop

在 main 中,我们新建了两个 CustomSmartPointer 实例并打印出了 CustomSmartPointer created.。在 main 的结尾,CustomSmartPointer 的实例会离开作用域,而 Rust 会调用放置于 drop 方法中的代码,打印出最后的信息。注意无需显示调用 drop 方法

当实例离开作用域 Rust 会自动调用 drop,并调用我们指定的代码。Rust 中的变量是按照它们在代码中定义的顺序被压入栈中的,也就是最后定义的变量会先被压入栈底,最先定义的变量会被压入栈顶。当作用域结束时,栈会按照后进先出(LIFO)的顺序弹出变量,因此先定义的变量会后被丢弃,后定义的变量会先被丢弃。所以 d 在 c 之前被丢弃。

结合文档示例,演示一个更复杂的案例:

struct CustomSmartPointer{
    data: String,
}

struct HasTwoDrops{
    one: CustomSmartPointer,
    two: CustomSmartPointer,
}


impl Drop for CustomSmartPointer{
    fn drop(&mut self){
        println!("Dropping CustomSmartPointer with data `{}`!", self.data);
    }
}

impl Drop for HasTwoDrops{
    fn drop(&mut self){
        println!("Dropping HasTwoDrops");
    }
}

fn main() {
    let c =  CustomSmartPointer{
        data: String::from("my stuff"),
    };
    let d = CustomSmartPointer{
        data: String::from("other stuff"),
    };
    let x = HasTwoDrops{ one: c, two: d };
    println!("Running!.");
}

运行图:

image-20230504115122511

通过 std::mem::drop 提早清理值

禁用自动执行 drop 并不是一件简单的事情。通常情况下,禁用 drop 并不必要;Drop trait 的整个意义就在于它会自动地处理。但若想要提早清理一个值。一个例子是在使用管理锁的智能指针时;你可能希望强制运行 drop 方法来释放锁以便作用域中的其他代码可以获取锁。Rust 不允许我们主动调用 Drop trait 的 drop 方法;当我们希望在作用域结束之前就强制释放变量的话,我们应该使用的是由标准库提供的 std::mem::drop

当直接调用 drop 方法:

image-20230504151630591

错误信息表明不允许显式调用 drop。错误信息使用了术语 析构函数(destructor),这是一个清理实例的函数的通用编程概念。析构函数 对应创建实例的 构造函数。Rust 中的 drop 函数就是这么一个析构函数。Rust 不允许我们显式调用 drop,因为 Rust 在 main 结束时仍会自动调用该值的 drop 方法。这将导致 double free 错误,因为 Rust 将尝试清理相同的值两次。

因为不能禁用当值离开作用域时自动插入的 drop,并且不能显示调用 drop,如果我们需要强制提早清理值,可以使用 std::mem::drop 函数。std::mem::drop 函数不同于 Drop trait 中的 drop 方法。可以通过传递希望提早强制丢弃的值作为参数std::mem::drop 位于 prelude。

image-20230504154241377

Drop trait 能够帮助开发者管理内存、清理资源,避免内存泄漏等问题。使用 Drop trait,你可以确保在你的类型被释放时执行特定的操作,而不需要手动清理资源,这样就能够避免出现一些常见的错误。需要注意的是,由于 Rust 确保每个值只会被清理一次,因此不能手动调用 Drop trait 的 drop 方法。如果需要在某个特定时刻清理一个值的资源,可以使用 std::mem::drop 函数来强制清理。

Rc 引用计数智能指针

Rust编程语言的所有权机制规定每个值只能有一个所有者,而所有者负责管理该值的内存分配和释放。在大多数情况下,所有权是明确的,可以准确地知道哪个变量拥有某个值。

但是,有些情况下单个值可能会有多个所有者。例如,在图数据结构中,多个边可能指向相同的结点,而这个结点从概念上讲为所有指向它的边所拥有。在这种情况下,可以使用 Rc<T> 类型来实现多个所有者的引用Rc<T> 是Rust语言提供的 引用计数 类型, 它允许多个所有者共享对同一数据的引用但它并不是对数据的所有权,而是对数据的共享引用计数的所有权。 Rc<T> 类型可以跟踪对值的 引用数,以确定该值是否仍在使用中。如果对值的引用计数为零,则该值可以被清理而不会使任何引用无效。

使用 Rc<T> 类型时,可以将数据分配在堆上,让多个部分共享读取。当我们不能在编译时确定哪个部分将最后完成使用数据时,这种方式就非常有用。但是需要注意的是, Rc<T> 类型仅适用于单线程场景,当涉及到多线程时,需要使用Arc<T> 类型,该类型是Rust语言提供的线程安全的引用计数类型。

使用 Rc 共享数据

实现两个(b,c)列表共享一个(a)列表功能,概念图如下:

trpl15-03

直接移动列表b,c 中:

use List::{Cons, Nil};

enum List {
    Cons(i32, Box<List>),
    Nil,
}

fn main() {
    let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
    let b = Cons(3, Box::new(a));
    let c = Cons(3, Box::new(a));
}

运行图:

image-20230504170145582

Cons 成员拥有其储存的数据,所以当创建 b 列表时,a 被移动进了 b 这样 b 就拥有了 a。接着当再次尝使用 a 创建 c 时,这不被允许因为 a 的所有权已经被移动。

使用 Rc<T> 代替 Box<T> 可以实现此概念。当创建一个新的列表项时,它会克隆前一个列表项的 Rc<T>,而不是获取其所有权。这样,每个列表项都有一个指向下一个列表项的 Rc<T>,从而构建了一个完整的列表。在这个过程中,Rc<T> 会维护一个引用计数,每当一个新的列表项被创建时,引用计数会增加。只有当所有的引用都被释放时,这些数据才会被清除。 当我们创建 b 时,我们将不再获取 a 的所有权,而是克隆 a 正在持有的 Rc<List>,从而将引用的数量从 1 增加到 2,并让 a 和 b 共享 Rc<List> 中数据的所有权。我们在创建 c 时也会克隆 a,将引用的数量从 2 增加到3。每次调用 Rc::clone 时,指向 Rc<List> 内部数据的引用计数都会增加,只有在引用计数为零时才会清除数据。

use List::{Cons, Nil};
use std::rc::Rc;

#[derive(Debug)]
enum List {
    Cons(i32, Rc<List>),
    Nil,
}

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    let b = Cons(3, Rc::clone(&a));
    let c = Cons(4, Rc::clone(&a));
    println!("{:?}", b);
    println!("{:?}", c);
    let d = Cons(8, Rc::new(b));
    println!("{:?}", d);
    println!("{:?}", a);
    // println!("{:?}", b);   // 失去所有权
}

运行图:

image-20230505104347962

首先使用 use 语句将 Rc<T> 引入作用域,因为它不在 prelude 中。

在 main 中创建了存放 5 和 10 的列表,并将其存储在新的 Rc<List> a 中。接着当创建 b 和 c 时,调用 Rc::clone 函数并传递 a 中 Rc<List> 的引用作为参数。也可以调用 a.clone() 而不是 Rc::clone(&a),不过在这里 Rust 的习惯是使用 Rc::cloneRc::clone 的实现并不像大部分类型的 clone 实现那样对所有数据进行深拷贝Rc::clone 只会增加引用计数。深拷贝可能会花费很长时间。通过使用 Rc::clone 进行引用计数,可以明显的区别深拷贝类的克隆和增加引用计数类的克隆。当查找代码中的性能问题时,只需考虑深拷贝类的克隆而无需考虑 Rc::clone 调用。

克隆 Rc 增加引用计数

use List::{Cons, Nil};
use std::rc::Rc;

#[derive(Debug)]
enum List {
    Cons(i32, Rc<List>),
    Nil,
}

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    println!("count after creating a = {}", Rc::strong_count(&a));
    let b = Cons(3, Rc::clone(&a));
    println!("count after creating b = {}", Rc::strong_count(&a));
    {
        let c = Cons(4, Rc::clone(&a));
        println!("count after creating c = {}", Rc::strong_count(&a));
    }
    let d = Cons(8, Rc::new(b));
    println!("count after c goes out of scope = {}", Rc::strong_count(&a));
}

运行图:

image-20230505110810968

通过调用 Rc::strong_count 函数获取引用计数, 这个函数叫做 strong_count 而不是 count 是因为 Rc 也有 weak_count(避免引用循环)。

image-20230505113452783

a:Rc<List> 的初始引用计数为1,接着每次调用 clone 方法都会将计数加1,而当c超出作用域时,计数会减1。不必像调用 Rc::clone 增加引用计数那样调用一个函数来减少计数。因为当 Rc<T> 值超出作用域时,Drop trait的实现会自动减少引用计数。

Rc 允许通过不可变引用来只读的在程序的多个部分共享数据。

RefCell 与内部可变性模式

内部可变性(Interior mutability)是 Rust 中的一个设计模式,它允许你即使在有不可变引用时改变数据,这通常是借用规则所不允许的。为了改变数据,该模式在数据结构中使用 unsafe 代码来模糊 Rust 通常的可变性和借用规则。内部可变性可以突破 Rust 编译期间的借用检查限制,因此需要在运行时确保遵守借用规则,以保证程序的正确性。并需要使用安全的 API(Rust 中一类特殊的函数或方法)来包装涉及不安全代码的操作。

unsafe 是一个关键字,用于标识一段代码块可能会违反语言的安全性保证。使用 unsafe 关键字可以告诉编译器,这段代码需要手动检查和确保遵循 Rust 的安全性规则,因为编译器不能保证代码的正确性。在 unsafe 代码块中,Rust 允许执行一些通常被禁止的操作,例如:解引用裸指针、进行指针算术运算、调用未定义行为的函数等。这些操作在不安全的情况下可能会导致内存破坏、数据竞争、未定义的行为等问题,因此必须谨慎使用。为了确保使用 unsafe 代码块时不会破坏 Rust 的安全性,通常需要使用安全的 API(Rust 中一类特殊的函数或方法) 和约束来封装 unsafe 代码,同时进行适当的检查和验证。这样可以保证程序在运行时不会出现问题,而且能够与 Rust 的类型和借用系统良好地集成。

RefCell 运行时遵守借用规则

Rc<T> 不同,RefCell<T> 类型表示对其所持有数据的单一所有权。但使用场景与 Rc<T> 类似,RefCell<T> 仅适用于单线程场景,如果尝试在多线程上下文中使用它,将会得到编译时错误。

如下为选择 Box,Rc 或 RefCell 的理由:

  • Rc<T> 允许相同数据有多个所有者;Box<T>RefCell<T> 有单一所有者。
  • Box<T> 允许在编译时执行不可变或可变借用检查;Rc<T> 仅允许在编译时执行不可变借用检查;RefCell<T> 允许在运行时执行不可变或可变借用检查,若违反这些规则程序会 panic 并退出。
  • 因为 RefCell<T> 允许在运行时执行可变借用检查,所以我们可以在即便 RefCell<T> 自身是不可变的情况下修改其内部的值。

在不可变值内部改变值就是 内部可变性 模式。

内部可变性:不可变值的可变借用

借用规则推论之一:对于一个不可变值,不能可变的借用它。

fn main() {
    let x = 5;
    let y = &mut x;
}
// error[E0596]: cannot borrow `x` as mutable, as it is not declared as mutable

这种情况下,使用 RefCell 可以实现内部可变性,但是在使用 RefCell 时仍然需要遵守 Rust 的借用规则,只是借用规则的检查由编译器从编译期转为运行期。如果违反了借用规则,程序将会 panic!

use std::cell::RefCell;

fn main() {
    let x = RefCell::new(5);
    {
        let y = &mut *x.borrow_mut();
        *y = 777;
    }
    println!("{}", x.borrow());
}

内部可变性的用例: Mock对象

测试替身(test double)

测试替身(test double)是一种在软件测试中使用的术语,指的是在测试过程中替代其他软件组件的一种技术,包括如下几种:

  1. Dummy objects: 这种测试替身并不会真正执行任何操作,而是作为占位符使用。通常用于方法参数中需要传递对象,但是这些对象在被调用的过程中并不实际被使用。
  2. Fake objects: 这种测试替身通常是一个轻量级的实现,与真实对象类似,但是实现起来更简单,通常只用于简单的测试场景中,例如在测试中使用内存中的哈希表而不是数据库中的真实数据。
  3. Stub objects: 这种测试替身通常是在测试中返回一个预设的值或执行一个预设的操作,以模拟真实的组件行为。通常用于测试中需要模拟返回值或模拟对象方法的行为。
  4. Mock objects: 这种测试替身与 Stub 对象类似,但是可以记录测试中发生的事件,以便于检查某些操作是否发生,或者检查某些参数是否正确。通常用于测试中需要检查某些操作是否发生或者参数是否正确的场景中。

总的来说,这四种测试替身的不同之处在于其在测试中的作用和使用场景。Dummy objects 通常用于方法参数的占位符;Fake objects 用于简单的测试场景;Stub objects 用于模拟对象方法的行为;而 Mock objects 则用于测试中需要检查操作是否发生或参数是否正确的场景。

rust mock对象

Rust 在对象方面与其他语言不同,Rust 标准库中没有像其他语言那样内置模拟对象的功能。然而,我们可以创建一个结构体,以实现与模拟对象相同的目的。

编写一个记录某个值与最大值的差距的库,并根据当前值与最大值的差距来发送消息。例如,这个库可以用于记录用户所允许的 API 调用数量限额。该库只提供记录与最大值的差距,以及何种情况发送什么消息的功能。使用此库的程序则期望提供实际发送消息的机制:程序可以选择记录一条消息、发送 email、发送短信等等。库本身无需知道这些细节;只需实现其提供的 Messenger trait 即可。库代码:

pub trait Messenger {
    fn send(&self, msg: &str);
}

pub struct LimitTracker<'a, T: Messenger> {
    messenger: &'a T,
    value: usize,
    max: usize,
}

impl<'a, T> LimitTracker<'a, T>
where T: Messenger, {
    pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T>{
        LimitTracker{
            messenger,
            value: 0,
            max,
        }
    }

    pub fn set_value(&mut self, value: usize){
        self.value = value;
        let percentage_of_max = self.value as f64 / self.max as f64;
        if percentage_of_max >= 1.0 {
            self.messenger.send("Error: You are over your quota!");
        } else if percentage_of_max >= 0.9 {
            self.messenger.send("Urgent warning: You've used up over 90% of your quota!");
        } else if percentage_of_max >= 0.75 {
            self.messenger.send("Warning: You've used up over 75% of your quota!");
        }
    }
}

定义一个 mock 以测试此 LimitTracker

use mock::{Messenger, LimitTracker};

#[cfg(test)]
mod tests{
    use super::*;
    struct MockMessenger {
        sent_messenger: Vec<String>,
    }
    impl MockMessenger {
        fn new() -> MockMessenger {
            MockMessenger {
                sent_messenger: vec![],
            }
        }
    }
    impl Messenger for MockMessenger{
        fn send(&self, message: &str){
            self.sent_messenger.push(String::from(message));
        }
    }

    #[test]
    fn it_sends_an_over_75_percent_warning_message() {
        let mock_messenger = MockMessenger::new();
        let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);

        limit_tracker.set_value(80);
        assert_eq!(mock_messenger.sent_messenger.len(), 1);
    }
}

运行此测试会发现以下问题:

image-20230506151937450

因为 send 方法获取 self 的不可变引用, 故无法修改 MockMessenger 来记录消息。若想通过修改 send 方法参数,传递 &mut self,也是不可以实现的。因为这样不符合 Messenger Trait 定义中的签名

error[E0053]: method `send` has an incompatible type for trait
  --> src\main.rs:17:17
   |
17 |         fn send(&mut self, message: &str){
   |                 ^^^^^^^^^
   |                 |
   |                 types differ in mutability
   |                 help: change the self-receiver type to match the trait: `self: &MockMessenger`
   |
   = note: expected fn pointer `fn(&MockMessenger, &str)`
              found fn pointer `fn(&mut MockMessenger, &str)`

For more information about this error, try `rustc --explain E0053`.
error: could not compile `mock` due to previous error

但若同时修改 lib.rs 中的下面三处,可以通过测试,但这不是合适的方式,更好的办法是使用 RefCell 的内部可变性特征来通过测试。

image-20230506162053309

使用内部可变性以通过测试

使用 RefCell<T> 来储存 sent_messages,这样 send 便能够修改 sent_messages 来储存消息。不需修改 lib.rs

image-20230506165553121

RefCell<T> 是一个实现了内部可变性模式的类型。它的主要特征包括:

  1. RefCell<T> 使用内部可变性模式允许在存在不可变引用的情况下修改数据,这是违反 Rust 借用规则的。
  2. 使用 borrow 和 borrow_mut 方法来管理对数据的借用,borrow 方法返回不可变引用,borrow_mut 方法返回可变引用。
  3. 当存在一个活动的 borrow 方法时调用 borrow_mut 方法会导致程序 panic。
  4. RefCell<T> 在运行时检查借用规则,如果不遵循规则则会导致 panic。
  5. RefCell<T>非线程安全的,如果需要在多线程环境下使用内部可变性模式,可以使用 Mutex<T>RwLock<T> 。(Cell<T> 非线程安全类型)

总之,RefCell<T> 允许在某些情况下使用内部可变性模式,但需要在运行时保证借用规则,同时需要注意线程安全性问题。

使用 RefCell 在运行时跟踪借用,保障 Rust 借用规则的正确性

当创建不可变和可变引用时,我们分别使用 &&mut 语法。对于 RefCell<T> 来说,使用 borrow 和 borrow_mut 方法可以分别创建 Ref<T>RefMut<T> 智能指针类型,属于 RefCell<T> 安全 API 的一部分。这两种类型都实现了 Deref,可以像处理常规引用一样处理它们。

RefCell<T> 会跟踪当前活动的 Ref<T>RefMut<T> 智能指针数量,就像编译时借用规则一样,RefCell<T> 在任何时候只允许有多个不可变借用一个可变借用。但如果我们违反了这些规则,RefCell<T> 的实现会在运行时 panic!。因此,在使用 RefCell 时,我们需要自己保证借用规则的正确性。

当一个作用域中同时创建可变引用和不可变引用:

fn main(){
    use std::cell::RefCell;
    let my_string = RefCell::new(String::from("hello"));
     let immutable_ref = my_string.borrow(); // 获取不可变引用
    let mutable_ref = my_string.borrow_mut(); // 获取可变引用
}

会产生以下错误:

thread 'main' panicked at 'already borrowed: BorrowMutError', src\main.rs:37:33
stack backtrace:
   0: rust_begin_unwind
             at /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library\std\src/panicking.rs:575:5
   1: core::panicking::panic_fmt
             at /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library\core\src/panicking.rs:64:14
   2: core::result::unwrap_failed
             at /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library\core\src/result.rs:1791:5
   3: core::result::Result<T,E>::expect
             at /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483\library\core\src/result.rs:1070:23
   4: core::cell::RefCell<T>::borrow_mut
             at /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483\library\core\src/cell.rs:958:9
   5: mock::main
             at .\src\main.rs:37:23
   6: core::ops::function::FnOnce::call_once
             at /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483\library\core\src\ops/function.rs:507:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
error: process didn't exit successfully: `target\debug\mock.exe` (exit code: 101)

image-20230508095522645

两个作用域不会产生此问题:

image-20230508095641126

在运行时捕获借用错误而不是编译时意味着将会在开发过程的后期才会发现错误,甚至有可能发布到生产环境才发现。还会因为在运行时而不是编译时记录借用而导致 RefCell<T> 会产生一些性能损失。然而,使用 RefCell 使得在只允许不可变值的上下文中编写修改自身以记录消息的 mock 对象成为可能。虽然有取舍,但是我们可以选择使用 RefCell<T> 来获得比常规引用所能提供的更多的功能。

通过结合 Rc 和 RefCell,可以拥有可变数据的多个所有者。

RefCell<T> 的一个常见用法是与 Rc<T> 结合。Rc<T> 允许对单个值有多个所有者,但它只提供对该数据的不可变访问。但如果有一个储存了 RefCell<T>Rc<T>,就可以得到有多个所有者并且可以修改的值了!

image-20230508140800939

RefCell, Cell, Mutex 和 RwLock

RefCell, Cell, Mutex 和 RwLock 都是 Rust 中用于管理共享数据的类型。它们有着相似的功能,但也有一些不同之处。下面是它们之间的相同和不同之处:

相同点:

  • 它们都提供了一种机制来管理共享数据,以防止数据竞争(data race)和其他并发问题。
  • 它们都提供了一种方式来实现内部可变性(Interior mutability)的模式,使得即使在数据被不可变地借用时也可以修改数据。
  • 它们都提供了一种类似于引用(reference)的方式来访问共享数据。这些类型允许在运行时检查数据的所有权和借用的规则,并根据规则进行访问。

不同点:

  • RefCell 和 Cell 是非并发的类型,它们都只能用于单线程场景。
  • Mutex 和 RwLock 是并发类型,它们都提供了线程间共享数据的同步机制。Mutex 允许一次只有一个线程可以访问共享数据,而 RwLock 允许多个线程同时读取数据,但只有一个线程可以写入数据。Mutex 和 RwLock 的实现会遵循内部可变性的规则,保证在共享数据被不可变借用的情况下,只有一个线程可以进行写入操作。同时,Mutex 和 RwLock 的实现还提供了一些附加的功能,如死锁检测等。

引用循环与内存泄漏

Rust的内存安全保证使得程序员难以意外地创建内存泄漏问题。虽然Rust不能完全防止内存泄漏的发生,但这并不会影响内存安全。在使用Rc和RefCell这两个Rust中的特殊数据类型时,我们可以证明Rust允许内存泄漏:程序员可能会创建循环引用,导致内存泄漏问题。这是因为在循环引用中,每个项目的引用计数永远不会降为0,导致它们的值永远无法被清理。

创建一个循环引用

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());
    println!("a = {:?}", a);

    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());
    println!("b = {:?}", b);
    if let Some(link) = a.tail() {
        *link.borrow_mut() = Rc::clone(&b);
    }
    // println!("a = {:?}", a);  // overflow the stack
    // println!("b = {:?}", b);  // overflow the stack
    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());
}

运行结果:

image-20230509144452972

引用循环示意图:

trpl15-04

a 指向 b 后,ab 中都有的 Rc<List> 实例的引用计数为 2。故 main 结束后,rust 会尝试丢弃 bbRc<List> 实例的引用计数由 2 减为 1,故 b 指向 Rc<List> 在堆上的内存不会释放。a 也一样,引用计数会减为 1,堆上的内存不会被丢弃。

循环引用不能通过 rust 发现,主要有两种办法来避免: 1. 需使用自动化测试、代码审查和其他软件开发实践来将其最小化。2. 重新组织数据结构,使一些引用表达所有权,而一些引用则不表达所有权。因此,你可以有由一些所有权关系和一些非所有权关系组成的循环,只有所有权关系会影响值是否可以被丢弃(不是所有引用计数都有用)。

防止引用循环:使用 Weak 代替 Rc

当使用 Rc::clone 方法时,一个 Rc<T> 实例的 strong_count 会加 1,只有当它的 strong_count 变为 0 时,Rc<T> 实例才会被清理。但我们可以使用 Rc::downgrade 方法来创建一个指向 Rc<T> 实例中值的弱引用(weak reference)。强引用(strong reference)是实现共享所有权的方式,而弱引用则不表达所有权关系,其计数也不影响 Rc<T> 实例的清理。使用弱引用不会引起引用循环,因为只要涉及到弱引用的循环,一旦与之相关的值的强引用计数变为 0,循环就会被打破。

调用 Rc::downgrade 方法会得到一个 Weak<T> 类型的智能指针。它不会像调用 Rc::clone 那样将 Rc<T> 实例的 strong_count 加 1,而是将 weak_count 加 1。与 strong_count 类似,Rc<T> 类型使用 weak_count 来跟踪有多少 Weak<T> 引用存在。不同之处在于,即使 weak_count 不为 0,Rc<T> 实例也可以被清理。

由于 Weak<T> 引用的值可能已经被丢弃,所以在操作 Weak<T> 指向的值之前,我们必须确保该值仍然存在。我们可以通过在 Weak<T> 实例上调用 upgrade 方法来实现这一点,它将返回一个 Option<Rc<T>>。如果 Rc<T> 值尚未被丢弃,你会得到 Some 结果;如果 Rc<T> 值已经被丢弃,你会得到 None 结果。由于 upgrade 返回一个 Option<Rc<T>>,所以 Rust 将确保处理 SomeNone 情况,并且不会出现无效指针。

创建树形数据结构:一个有子节点的节点

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)]),
    });
    println!("leaf is {:?}, \nbranch is {:?}", leaf, branch);
}

运行图:

image-20230510171737064

我们在叶节点中克隆了 Rc<Node> 并将其存储在branch节点中,这意味着leaf节点现在有两个所有者:leafbranch节点。我们可以通过 branch.childrenbranch节点访问到leaf节点,但是无法从leaf节点访问到branch节点。原因是leaf节点没有指向branch节点的引用,也不知道它们之间的关系。

向父节点添加引用

为使子节点与父节点建立关系,在 Node 结构体定义中添加一个 parent 字段。但是,若 parent 类型为 Rc<T> 会创建一个引用循环,因为 leaf.parent 指向 branch,而 branch.children 指向 leaf,这将导致它们的 strong_count 值永远不为 0。

从另一个角度考虑关系,父节点应该拥有它的子节点:如果父节点被丢弃,它的子节点也应该被丢弃。但是,子节点不应该拥有其父节点:如果我们丢弃一个子节点,父节点应该仍然存在。这是一个**弱引用(weak references)**的情况!

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());
    println!("leaf is {:?}, \nbranch is {:?}", leaf, branch);
}

运行图:

image-20230511103910973

由于 Weak<T> 引用的值可能已经被丢弃,故而需使用 upgrade 确保该值仍然存在。同样因为 Weak<T> 类型是一种弱引用类型,不能保证所引用的对象在内存中仍然存在,因此在打印 Weak<Node> 类型的节点时,它们只会被打印为 (Weak),而不会显示节点的值。Rc::downgrade 方法会得到 branch 一个 Weak<T> 类型的智能指针。

可视化strong_count和weak_count

通过内部作用域,来观察 strong_countweak_count 变化。

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 is {:?}, \nbranch is {:?}", leaf, branch);
    }
    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
    println!("leaf strong = {}, weal = {}", Rc::strong_count(&leaf), Rc::weak_count(&leaf));
}

运行图:

image-20230511134240718

创建 leaf 节点时,它的 Rc<Node> 强引用计数为1,弱引用计数为0。在内部作用域中,创建 branch 并将其与 leaf 节点关联。此时 branch 中的 Rc<Node> 强引用计数为1和弱引用计数为1(因为leaf.parent为指向branch的一个 Weak<Node>)。leaf 的强引用计数为2(因为branch存储了一个Rc<Node>的克隆,指向leaf节点的Rc<Node>),但弱引用计数仍为0。

当内部作用域结束时,branch 超出作用域,Rc<Node> 的强引用计数减少为0,故 Node 被丢弃。leaf.parent 的1个弱引用计数不影响 Node 是否被丢弃,故不会出现内存泄漏!作用域结束后访问 leaf 节点的父节点,会得到None。此时 leaf 节点中的 Rc<Node> 强引用计数为1,弱引用计数为0,因为现在leaf 节点是 Rc<Node> 唯一的引用。