Back
Featured image of post Rust 学习笔记08 - 智能指针

Rust 学习笔记08 - 智能指针

导航页

指针(Pointer)是一个包含内存地址变量的通用概念。这个地址引用,或谓「指向」一些其他的数据。Rust 中最常见的指针是引用(Reference)。引用以 & 符号标识它们借用所指的值。引用很简单,也没有特殊功能,所以用得最多。

另一方面,智能指针(smart pointers)是一类特殊的数据结构,表现类似指针,但拥有额外的元数据和功能。智能指针这个概念起源于 C++ 并在其他语言中流行,Rust 也不例外。

值得注意的是,在 Rust 中,普通指针和智能指针最显著的区别是:引用只借用数据;智能指针拥有它们所指的数据。

实际上,String Vec<T> 这种数据类型,在广义上也可以称之为智能指针(虽然没人这么叫)。的确拥有数据所有权,并有额外的元数据和功能。

智能指针通常使用结构体实现,并实现 DerefDrop trait。Deref 允许智能指针结构体表现得像引用一样,这样就可以编写既用于引用、又用于智能指针的代码。Drop trait 我们之前已经涉及到,允许结构体在离开作用域时自动清除。

Box<T>

Box<T> 是最简单直接的智能指针,它允许你将一个值放在堆上,而不是栈上。留在栈上的指向堆的指针。常用于以下场景:

  • 有一个编译时大小未知的类型
    • Vec HashMap String 肯定都用到了 Box<T>
  • 数据很大,我们希望数据不被拷贝,仅转移所有权
  • 当拥有一个值,只关心它的类型是否实现了特定的 trait 而非具体类型时
    • 在之前我们介绍过 trait 的缺陷,可以利用 Box<T> + dyn 关键字可以实现 trait 对象解决这个问题,不过本篇暂时还不会介绍

基本语法

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

很简单,定义后使用即可。当然,b 也遵守所有权规则。在 main 函数结束时,它将被释放,作用于 box 本身(在栈上)和它所指的数据(在堆上)。

但这样将一个单独的值存放在堆上意义不大。下面我们将介绍一个实际的用例。

创建递归类型

Rust 需要在编译时知道类型占用的空间。一种无法在编译时知道大小的类型是递归类型(recursive type),其值的一部分,可以是相同类型的另一个值。这种值的嵌套可以无限进行。所以 Rust 并不知道递归类型所需的类型。但只要使用 Box<T>,该值在栈上的大小就是固定的,自然就可以创建递归类型了。

让我们定义一个 cons list:

enum List {
  Cons(i32, List),
  Nil,
}

为了示例尽可能简单,这里就直接用具体类型 i32 了。当然也可以使用泛型,来创建可以存放任何类型的 cons list。

我们期望这样使用它:

use crate::List::{Cons, Nil};

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

然而遗憾的是,上述代码会编译错误。

error[E0072]: recursive type `List` has infinite size
|
3 | enum List {
  | ^^^^^^^^^ recursive type has infinite size
4 |   Cons(i32, List),
  |             ---- recursive without indirection
  |
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to make `List` representable
  |
4 |   Cons(i32, Box<List>),
  |             ++++    +

正如错误信息所描述的,这个类型有「无限的大小」。栈上的空间必须是已知的。

所以,我们可以间接地存储一个值,如使用 Box(建议中还给出了 Rc ,这在后面介绍)。

这样就好了:

use crate::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))))));
}

Deref trait

Deref trait 允许我们重载解引用操作符(dereference operator)*。所以实现 Deref 的智能指针可以当作常规引用看待。可以编写操作引用的代码,并用于智能指针。

通过解引用追踪指针的值

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

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

很简单的示例,x 存放了一个 i32 值。y 绑定 x 的引用。可以断言 x == 5。然而如果希望对 y 的值做出断言,我们则需要用 *y 追踪引用所指的值(解引用)。一旦解引用了 y,就可以访问 y 所指的 i32 类型值,并和 5 比较了

像引用一样使用 Box<T>

通过 Deref trait,使用 Box<T> 的方式惊人的相似:

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

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

这正是 Deref 特性的方便之处,减少了很多重复工作。

自定义智能指针

这里的 MyBox<T> 用作示例,实际上和普通指针的功能一样,都是在栈上分配空间。

不妨实现一个 MyBox<T> 智能指针:

struct MyBox<T>(T);

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

现在使用解引用操作符会报错:

fn main() {
  let my_box = MyBox::new(13);
  assert_eq!(*my_box, 13);
}
error[E0614]: type `MyBox<{integer}>` cannot be dereferenced
   |
11 |   assert_eq!(*my_box, 13);
   |              ^^^^^^^

我们可以实现 Deref

use std::ops::Deref;

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

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

其中 type Target = T; 的语法尚未遇到,不过这不影响理解。这留到后面去介绍。只需要知道 &Self::Target 和直接使用 T 类似即可。

这样,之前的代码就可以编译通过了。

实际上,Rust 在幕后将 *my_box 替换为了 *(my_box.deref())。很方便的语法糖。

请注意,* 操作符只会解一次引用,不会无限递归替换。也就是说多个引用或智能指针需要多次解引用。

Deref 隐式强转

Deref 强制转换(deref coercions)是在函数或方法传参上的一种便利。用于实现了 Deref trait 的类型。将一个类型的引用,转为另一个类型的引用。例如 &String&str,因为 String 实现了 Deref trait,因此可以返回 &str

这减少了反复显式使用 & * 可能,也使得我们可以编写更通用的代码。

参看以下示例,这里使用了之前定义的 MyBox<T>

fn hello(name: &str) {
    println!("Hello, {}!", name);
}

fn main() {
    let m = MyBox::new(String::from("Rust"));
    hello(&m);
}

这里调用了两次不同类型的 deref。先是将 &MyBox<String> 类型变为 &String,之后又将其变为 &str

如果 Rust 没有该特性,可能要这样写:

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

(*m)MyBox<String> 解引用为 String,接着 &[..] 获取了整个 String 字符串的 slice 来匹配 hello 的函数签名 &str。一堆符号混在一起难以阅读和理解。

所以有了 Deref 我们可以更轻松地使用智能指针,且没有运行时损耗(解析发生在编译时)。

和可变性交互

类似于 Deref 可以重载不可变引用的 * 运算符,Rust 也提供了 DerefMut 用于重载可变引用的 *

Rust 在以下情况强制转换:

  • T: Deref<Target=U> 时,&T&U
  • T: DerefMut<Target=U> 时,&mut T&mut U
  • T: Deref<Target=U> 时,&mut T&U

头两个情况没什么解释的。第三种情况很微妙:

Rust 会将可变引用转为不可变的。但是反之是不可能的。根据借用规则,如果有一个可变引用,其必须是这些数据的唯一引用。将可变强转为不变不会打破借用规则;但不变转为可变,需要假定该不变引用是该数据唯一的引用,而借用规则无法保证这一点。所以 Rust 不允许将不可变引用转换为可变引用。

Drop trait

Drop trait 允许值离开作用域时执行一些代码,可以为任何类型指定 Drop 的实现。可以将该特性用于释放文件或网络连接。我们在此讨论该 trait,是因为它总是用于实现智能指针。例如,当 Box<T> 被丢弃时会释放 box 指向的堆空间。

在其他语言中,可能需要在使用智能指针实例后,手动调用清理内存或资源的代码,如果忘记清理,运行代码的系统很有可能因为负担过重崩溃。但 Rust 使用该方式能够自动在实例结束时清理,并且没有资源泄漏。

不妨查看以下代码:

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.");
}

这里我们用 println() 替代实际清理资源的逻辑。在变量离开作用域时,会自动调用 drop(),所以输出如下:

CustomSmartPointers created.
Dropping CustomSmartPointer with data `other stuff`!
Dropping CustomSmartPointer with data `my stuff`!

提前调用 drop

Rust 不允许我们手动调用 drop() 方法,但我们可能切实需要提早清理某个值。典型例子是在使用智能指针管理锁时,我们会希望提前调用 drop 释放锁使得其他代码可以获取锁。所以可以使用 std::mem::drop 提早丢弃值。

以下代码会报错:

fn main() {
    let c = CustomSmartPointer {
        data: String::from("some data"),
    };
    println!("CustomSmartPointer created.");
    c.drop();
    println!("CustomSmartPointer dropped before the end of main.");
}
error[E0040]: explicit use of destructor method
   |
16 |   c.drop();
   |   --^^^^--
   |   | |
   |   | explicit destructor calls not allowed
   |   help: consider using `drop` function: `drop(c)`

错误信息表明,我们提前使用了析构函数(destructor),这是和构造函数(constructor)相反的概念。Rust 中的 drop 就是一种析构函数。

报错是因为这会引起 double free 错误,程序会尝试清理两次同样的值。

但可以用 std::mem::drop,因为它的语义是「提早清理」,也就是说在作用域结束时,不会再调用一次。

该函数位于预导入中,所以可以不用导包:

fn main() {
    let c = CustomSmartPointer {
        data: String::from("some data"),
    };
    println!("CustomSmartPointer created.");
    drop(c);
    println!("CustomSmartPointer dropped before the end of main.");
}

现在的打印信息如下:

CustomSmartPointer created.
Dropping CustomSmartPointer with data `some data`!
CustomSmartPointer dropped before the end of main.

Rc<T> 引用记数

有时候为了实现多所有者,我们需要使用引用记数(reference counting),在 Rust 中对应的类型是 Rc<T>

这意味会记录用一个值记录引用的数量,来判断是否仍在被使用。如果某个值有零个引用,就代表没有任何有效引用,可以被清理。

当我们希望一个值供程序中多个部分使用时,且不知道那一部分会先结束,我们就可以用 Rc<T> 实现。

注意:Rc<T> 只能用于单线程场景。

假设我们要实现以下数据结构。

Box<T> 不能实现该数据结构。

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

use crate::List::{Cons, Nil};

fn main() {
    let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
    let b = Cons(3, Box::new(a));
    let c = Cons(4, Box::new(a));
}
error[E0382]: use of moved value: `a`
  --> src/bin/playground.rs:11:28
   |
9  |   let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
   |       - move occurs because `a` has type `List`, which does not implement the `Copy` trait
10 |   let b = Cons(3, Box::new(a));
   |                            - value moved here
11 |   let c = Cons(4, Box::new(a));
   |                            ^ value used here after move

Cons 成员拥有其储存的数据,所以当创建列表 b 时,a 已经移动去了 b,这样 b 就有了 a。接着当 c 使用 a 时,就会报错,因为 a 的所有权已经被移动。

可以改变 Cons 的定义来存放引用,然后指定生命周期参数。但这不是普遍的情况。

相反,我们可以修改 Box<T>Rc<T>。如下所示:

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

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

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil))))); // rc + 1
    let b = Cons(3, Rc::clone(&a)); // rc + 1
    let c = Cons(4, Rc::clone(&a)); // rc + 1
}

使用引用计数,将允许多个值共享所有权。

Rc::clone() 不会获取所有权,但也不会深拷贝整个列表,只是增加引用记数。

下面的例子可以更清晰地看出引用记数值的变化:

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));
    }
    println!("count after c goes out of scope = {}", Rc::strong_count(&a));
}
count after creating a = 1
count after creating b = 2
count after creating c = 3
count after c goes out of scope = 2

在程序中每个引用记数变化的点,会打印出引用记数,可以通过 Rc::strong_count() 获取,叫 strong_count 而非 count,是因为 Rc<T> 也有 weak_count

我们能够看到每次 clone,引用记数值都会增加 1。当 c 离开作用域时,记数减 1。不必像调用 Rc::clone 一样来手动操作记数减少,因为实现了 Drop trait,所以会在离开作用域时自动减少。

Rc<T> 只允许在程序的多个部分之间只读地共享数据。若 Rc<T> 允许多个可变引用,则会违反之前讨论过的借用规则:相同位置的多个可变借用可能造成数据竞争和不一致。

RefCell<T> 和内部可变性

内部可变性(Interior mutability)是 Rust 中的一个设计模式。允许在不可变引用存在时,也可以改变数据。为了改变数据,该模式会使用 unsafe 代码模糊 Rust 通常的可变性和借用规则。将 unsafe 代码封装进安全的 API 中,这就是 RefCell<T> 所做的。

不同于 Rc<T>RefCell<T> 代表其数据的唯一所有权。那么为何 RefCell<T> 不同于 Box<T> 呢?

答案很简单,对于引用和 Box<T>,借用规则的不可变性作用于编译时。但对 RefCell<T> 是作用于运行时。对于引用,会引发编译时错误;而对于 RefCell<T> 会在运行时 panic 并退出。

编译时检查借用的优势在于,这些错误会在编译时被捕获,并且对性能没有影响。所以这通常是最佳选择,也是 Rust 的默认行为。

相反在运行时检查,会带来动态性的好处,有些场景在运行时是内存安全的,但在编译检查中却不允许。静态分析,如 Rust 编译器,是天生保守的。代码的一些属性不可能通过分析代码发现,其中最著名的就是停机问题。

RefCell<T> 用于你确信代码遵循借用规则,但编译器却不能理解和确定的时候。

类似 Rc<T> RefCell<T> 也只能用于单线程场景。

在不可变值内部改变值就是内部可变性模式。不妨查看一个实际的例子。

Mock 对象

测试替身(test double)是一个通用编程概念,它代表一个在测试中替代某个类型的类型。mock 对象是特定类型的测试替身,它们记录测试过程中发生了什么以便可以断言操作是正确的。

查看以下代码,我们要记录一个值与最大值的差距,并根据该差距发送消息。例如,可以用于记录用户所允许的 API 调用限额。

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: &T, max: usize) -> LimitTracker<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 对象。

#[cfg(test)]
mod tests {
  use super::*;

  struct MockMessenger {
    sent_messages: Vec<String>,
  }

  impl MockMessenger {
    fn new() -> MockMessenger {
      MockMessenger {
        sent_messages: vec![],
      }
    }
  }

  impl Messenger for MockMessenger {
    fn send(&self, message: &str) {
      self.sent_messages.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_messages.len(), 1);
  }
}

不过以上代码无法通过编译。编译器抱怨 self.sent_messages 不可变,所以无法调用 push

我们无法将 send 的签名改变为 &mut self,因为它们是公共 API 的一部分。所以我们可以用 RefCell 来储存 sent_messages

#[cfg(test)]
mod tests {
  use super::*;
  use std::cell::RefCell;

  struct MockMessenger {
    sent_messages: RefCell<Vec<String>>,
  }

  impl MockMessenger {
    fn new() -> MockMessenger {
      MockMessenger {
        sent_messages: RefCell::new(vec![]),
      }
    }
  }

  impl Messenger for MockMessenger {
    fn send(&self, message: &str) {
      self.sent_messages.borrow_mut().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_messages.borrow().len(), 1);
  }
}

对于 RefCell<T> 来说,我们可以使用 borrowborrow_mut 对应 &&mut,分别返回 Ref<T>RefMut<T>。这两个类型都实现了 Deref,所以可以当作常规引用对待。

RefCell<T> 内部记录了有多少个 Ref<T>RefMut<T> 智能指针。每次调用 borrow,不可变借用记数加一,离开作用域时减一。就像是编译时的规则一样,RefCell<T> 允许多个不可变引用,或者一个可变引用。

如果违反规则,则会在运行时 panic。比如这样:

impl Messenger for MockMessenger {
  fn send(&self, message: &str) {
    let mut one_borrow = self.sent_messages.borrow_mut();
    let mut two_borrow = self.sent_messages.borrow_mut();

    one_borrow.push(String::from(message));
    two_borrow.push(String::from(message));
  }
}
already borrowed: BorrowMutError
thread 'tests::it_sends_an_over_75_percent_warning_message' panicked at 'already borrowed: BorrowMutError', src/main.rs:62:47
// ....

结合 Rc<T>RefCell<T>

Rc<T> 允许多个不可变引用,而 RefCell<T> 允许内部可变性。如果将两者组合,我们就可以获得多个可变数据的所有者:

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

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

fn main() {
  let value = Rc::new(RefCell::new(5));

  let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));

  let b = Cons(Rc::new(RefCell::new(3)), Rc::clone(&a));
  let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a));

  *value.borrow_mut() += 10;

  println!("a after = {:?}", a);
  println!("b after = {:?}", b);
  println!("c after = {:?}", c);
}

输出如下:

a after = Cons(RefCell { value: 15 }, Nil)
b after = Cons(RefCell { value: 3 }, Cons(RefCell { value: 15 }, Nil))
c after = Cons(RefCell { value: 4 }, Cons(RefCell { value: 15 }, Nil))

循环引用和内存泄漏

Rust 的内存安全性保证难以意外地制造内存泄漏,但并非完全不可能。在编译时拒绝数据竞争不同,Rust 并不保证完全地避免内存泄漏,也就是说,Rust 认为内存泄漏是内存安全的。可以通过 Rc<T>RefCell<T> 看出:创建循环引用的可能性存在。

制造循环引用

我们可以定义和之前类似的 cons list:

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,
    }
  }
}

我们使用 RefCell<Rc<List>> 定义 Cons 的第二个元素,这意味着可以修改 Cons 所指向的 List,还有一个 tail 方法来方便我们在有 Cons 成员时访问其第二项。

以下代码会产生循环引用:

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));

    // println!("a next item = {:?}", a.tail());
}

输出:

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

ba 的引用,而 a 也有 b 的引用。在作用域结束后,ab 的记数值(2)减 1,仍然为 1。两个值互相维系,让值永远大于 1。

如果取消最后 println! 的注释并运行程序,Rust 会尝试打印出 a 指向 b 指向 a 这样的循环直到栈溢出。

本例中,循环引用的结果并不可怕。但如果在更为复杂的系统中出现了内存泄漏,可能会造成性能下降甚至崩溃。

创建循环引用并不容易,但并非不可能。再使用 Rc<T>RefCell<T> 值或类似的嵌套结合了内部可变性和引用计数的类型,应注意避免循环引用。创建引用循环是一个程序上的逻辑 bug,应该使用测试、code review 或其他方式最小化。

另一个解决方案是重新组织数据结构,使得一些引用拥有所有权。但在本例中是不可能的,我们希望 Cons 成员拥有其列表。

使用 Weak<T>

我们知道 Rc::clone 会增加 Rc<T> 实例的 strong_count,并且 Rc<T> 只在 strong_count 为 0 时才会被清理。

我们还可以调用 Rc::downgrade 并传入 Rc<T> 实例的引用创建其值的弱引用(weak reference)。调用 Rc::downgrade 时会得到 Weak<T> 的智能指针。不同于将 Rc<T> 实例的 strong_count 加 1。调用 Rc::downgrade 会使得 weak_count 加 1。Rc<T> 类型使用 weak_count 来记录其存在多少个 Weak<T> 引用,区别仅在于 weak_count 无需计数为 0 就能使 Rc<T> 实例被清理。

强引用代表如何共享 Rc<T> 实例的所有权,但弱引用并不属于所有权关系。不会造成引用循环,因为任何弱引用的循环,会在强引用计数为 0 时被打断。

因为 Weak<T> 所引用的值可能已经被丢弃了,所以为了使用 Weak<T> 所指向的值,我们必须保证值仍然有效。可以使用 Weak<T>upgrade 方法获得一个 Option<Rc<T>>。若 Rc<T> 的值未被丢弃,则结果是 Some;若已经被丢弃,则结果是 None。因为 upgrade 返回的是一个 Option<Rc<T>>,所以 Rust 可以保证 SomeNone,不会返回非法指针。

创建树形结构

我们可以定义一个 Node 节点:

use std::cell::RefCell;
use std::rc::Rc;

#[derive(Debug)]
struct Node {
    value: i32,
    children: RefCell<Vec<Rc<Node>>>,
}

我们希望 Node 有子节点,并通过变量来共享所有权,以便于访问树中每一个 Node,为此 Vec<T> 的类型为 Rc<Node>。我们还希望能修改其他节点的子节点,所以 childrenVec<Rc<Node>> 被放进了 RefCell<T>

创建一个简单的树实例:

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)]),
    });
}

这里克隆了 leaf 中的 Rc<Node> 并储存在了 branch 中,这意味着 leaf 中的 Node 现在有两个所有者:leafbranch。可以通过 branch.childrenbranch 中获得 leaf,不过无法从 leafbranchleaf 没有到 branch 的引用且并不知道他们相互关联。

增加子到父的引用

我们可以更改定义为:

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());
}

输出为:

leaf parent = None
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 调用的结果看出。

可视化强弱引用记数的改变

可以创建了一个新的内部作用域并将 branch 的创建放入其中,来观察 Rc<Node> 实例的 strong_countweak_count 值的变化:

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),
  );
}

输出为:

leaf strong = 1, weak = 0
branch strong = 1, weak = 1
leaf strong = 2, weak = 0
leaf parent = None
leaf strong = 1, weak = 0

一旦创建了 leaf,强引用记数就为 1,弱引用记数就为 0。在内部作用域中创建了 branch 并与 leaf 相关联,此时 branchRc<Node> 的强引用计数为 1,弱引用计数为 1(因为 leaf.parent 通过 Weak<Node> 指向 branch)。这里 leaf 的强引用计数为 2,因为现在 branchbranch.children 中储存了 leafRc<Node> 的拷贝,不过弱引用计数仍然为 0。

当内部作用域结束时,branch 离开作用域,Rc<Node> 的强引用计数减少为 0,所以其 Node 被丢弃。来自 leaf.parent 的弱引用计数 1 与 Node 是否被丢弃无关,所以并没有产生任何内存泄漏。

如果在内部作用域结束后尝试访问 leaf 的父节点,会再次得到 None。在程序的结尾,leafRc<Node> 的强引用计数为 1,弱引用计数为 0,因为现在 leaf 又是 Rc<Node> 唯一的引用了。

总结

这里总结一下这几种智能指针的常见目的:

  • Box<T> 能够指向堆上的数据
  • Rc<T> 允许数据有多个不可变的所有者
  • RefCell<T> 使用内部可变性模型,允许在运行时检查借用,在自身不变的情况下改变其内部的值
comments powered by Disqus