rust 快速入门——17 智能指针

133 阅读39分钟

[!|center] 普若哥们儿

github.com/wu-hongbing…

gitee.com/wuhongbing/…

智能指针

指针pointer)是一个包含内存地址的变量的通用概念。Rust 中最常见的指针是 引用reference),引用以 & 符号为标志并借用了它们所指向的值。引用 除了引用数据没有任何其他特殊功能,也没有额外开销。

智能指针smart pointers)是一类数据结构,它们的表现类似指针,但是也拥有额外的元数据和功能。智能指针的概念并不为 Rust 所独有,其起源于 C++ 并存在于其他语言中。

Rust 存在借用的概念,普通引用和智能指针的一个额外的区别是引用是一类只借用数据的指针;相反,在大部分情况下,智能指针 拥有 它们指向的数据。

实际上之前的内容已经出现过一些智能指针,比如 StringVec<T>,这些类型都属于智能指针,它们拥有一些数据并允许修改。它们也拥有元数据和额外的功能或保证,例如 String 存储了其容量作为元数据,并拥有额外的能力确保其数据总是有效的 UTF-8 编码。

智能指针通常使用结构体实现,不同于结构体的地方在于其实现了 DerefDrop trait。Deref trait 允许智能指针结构体实例表现的像引用一样,这样就可以编写既用于引用、又用于智能指针的代码。Drop trait 允许我们自定义当智能指针离开作用域时运行的代码。

本章介绍标准库中最常用智能指针:

  • Box<T>,用于在堆上分配值
  • Rc<T>,一个引用计数类型,其数据可以有多个所有者
  • Ref<T>RefMut<T>,通过 RefCell<T> 访问。( RefCell<T> 是一个在运行时而不是在编译时执行借用规则的类型)。

另外还会介绍 内部可变性interior mutability)模式,这是不可变类型暴露出改变其内部值的 API;还会讨论 引用循环reference cycles)会如何泄漏内存,以及如何避免。

使用 Box<T> 指向堆上的数据

最简单的智能指针是 box,其类型是 Box<T>。box 允许将一个值放在堆上而不是栈上,留在栈上的则是指向堆数据的包含指针的结构体,其内存大小是固定的,除此以外 box 并没有很多额外的功能。它们多用于如下场景:

  • 当有一个在编译时未知大小的类型 T,Rust 无法直接在栈上创建其变量,可以在栈上创建 Box<T>,在堆上创建 T,由 Box<T> 中的指针指向它
  • 当有大量数据并希望在确保数据不被拷贝的情况下转移所有权的时候。(普通引用只是借用,而不是转移)
  • 当希望拥有一个值并只关心它的类型是否实现了特定 trait (trait 对象trait object))而不是其具体类型的时候

使用 Box<T> 在堆上储存数据

下例展示了如何使用 box 在堆上储存一个 i32

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

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

单个 i32 这样的值储存在栈上,将其存放在堆上并不是很有意义。但是 Box<T> 中的泛型类型 T 意味着可以像这样处理任何复杂的、内存尺寸不定的类型。

Box 允许创建递归类型

递归类型recursive type)的值可以拥有另一个同类型的值作为其自身的一部分。下面是一个递归的例子,先定义一个枚举类型 List

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

使用 List 来储存列表 1, 2, 3 将看起来如下例所示:

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

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

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

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

1793e1510f84781e9e1a27619913cf58.svg

图 :一个包含无限个 Cons 成员的无限 List

如果尝试编译示例会得到错误:

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

这个错误表明这个类型 “有无限的大小”。其原因是 List 的一个成员被定义为是递归的:它直接存放了另一个相同类型的值。这意味着 Rust 无法计算为了存放 List 值到底需要多少空间,也就无法创建该类型的变量。

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

Box<T> 是一个指针,我们总是知道它需要多少空间:指针的大小并不会根据其指向的数据量而改变。这意味着可以将 Box 放入 Cons 成员中而不是直接存放另一个 List 值。Box 会指向另一个位于堆上的 List 值,而不是存放在 Cons 成员中。

可以这样设计:

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

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

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

Cons 成员将会需要一个 i32 的大小加上储存 box 指针数据的空间。Nil 成员不储存值,所以它比 Cons 成员需要更少的空间。现在我们知道了任何 List 值最多需要一个 i32 加上 box 指针数据的大小。通过使用 box,打破了这无限递归的连锁,这样编译器就能够计算出储存 List 值需要的大小了。下图展示了现在 Cons 成员的结构:

e0cbc070e7a20ad391baa3ac1909dac9.svg

图:因为 Cons 存放一个 Box 所以 List 不是无限大小的了

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

通过 Deref trait 将智能指针当作常规引用处理

实现 Deref trait 允许我们重载 解引用运算符dereference operator*(不要与乘法运算符或通配符相混淆)。通过这种方式实现 Deref trait 的智能指针可以被当作常规引用来对待,可以编写操作引用的代码并用于智能指针。

像引用一样使用 Box<T>

常规引用是一个指针类型,可以通过解引用运算符 * 作用引用来获取引用指向的值。

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

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

变量 x 存放了一个 i325y 等于 x 的一个引用。第 6 行,通过解引用运算符 * 获取了 y 指向的值 5

可以使用 Box<T> 代替引用来重写上面的代码:

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

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

y 为一个指向 x 值的拷贝的 Box<T> 实例,而不是指向 x 值的引用。在第 6 行中,采用与上例相同的形式,对 Box<T> 类型的值使用解引用运算符 * 获取 Box<T> 指向的值。

接下来让我们通过实现自己的类型来解释 Box<T> 为什么能这么做。

自定义智能指针

从根本上说,Box<T> 被定义为包含一个元素的元组结构体,所以下例以相同的方式定义了 MyBox<T> 类型。我们还定义了 new 函数来对应定义于 Box<T>new 函数。

Deref trait,由标准库提供,要求实现名为 deref 的方法,其借用 self 并返回一个内部数据的引用。下例包含定义于 MyBox 之上的 Deref 实现。

use std::ops::Deref;

struct MyBox<T>(T);

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

    fn deref(&self) -> &Self::Target {
        &self.0   // 返回的是值的引用
    }
}

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

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

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

type Target = T; 语法定义了 Deref trait 的关联类型。第 9 行, deref 方法中的 &self.0 返回了 MyBox<T> 中的值的引用,对这个引用使用 * 运算符即可得到值本身。因此 deref 方法向编译器提供了获取任何实现了 Deref trait 的类型的值能力。当我们在示例中输入 *y 时,Rust 编译器实际生成如下代码:

*(y.deref())

Rust 的这个特性可以让我们写出行为一致的代码,无论是面对的是常规引用还是实现了 Deref 的类型。

注意, deref 方法返回的是值的引用,如果 deref 方法直接返回值而不是值的引用,其值(的所有权)将被移出 self。在这里以及大部分使用解引用运算符的情况下并不希望获取 MyBox<T> 内部值的所有权。

函数和方法的隐式 Deref 转换

一个实现 Deref trait 的类型的 deref 方法理论上可以返回任意的类型,比如 A 类型的 deref 方法可以返回 &B 类型。

在调用函数或方法时,如果实参和形参的引用类型不一致,比如实参类型为 &A,形参类型为 &B, Rust 编译器会查看实参的 deref 方法的返回值是否与形参一致,如果一致,则隐式地为调用实参的 deref 方法完成转换。比如:

fn main() {
    let s = String::from("hello world");
    display(&s)
}

fn display(s: &str) {
    println!("{}",s);
}

在标准库中 String 实现了 Deref trait , deref 方法返回类型为 &str。在第 3 行,Rust 隐式地调用 deref 方法,将变量 s&String 类型转换为 &str 类型。

当所涉及到的类型定义了 Deref trait,Rust 会分析这些类型并调用任意多次 Deref::deref 以获得匹配参数的类型。这些解析都发生在编译时,所以利用 Deref 隐私转换并没有运行时损耗!比如:

fn main() {
    let s =5;
    display(&&&&s); // 需要多次解引用
}

fn display(s: &i32) { // 多次调用 `Deref::deref` 以获得匹配参数的类型
    println!("{}",s);
}

Deref 转换使得 Rust 程序员编写函数和方法调用时无需增加过多地显式使用 &* 运算符进行引用和解引用。这个功能也使得我们可以编写更多同时作用于引用或智能指针的代码。

Deref 转换如何与可变性交互

前面讲的是 Deref trait 重载不可变引用的 * 运算符,除此之外,Rust 还提供了 DerefMut 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 无法假设将不可变引用转换为可变引用是可能的。

来看一个关于 DerefMut 的例子:

struct MyBox<T> {
    v: T,
}

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

use std::ops::Deref;

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

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

use std::ops::DerefMut;

impl<T> DerefMut for MyBox<T> {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.v
    }
}

fn main() {
    let mut s = MyBox::new(String::from("hello, "));
    display(&mut s)
}

fn display(s: &mut String) {
    s.push_str("world");
    println!("{}", s);
}

以上代码有几点值得注意:

  • 要实现 DerefMut 必须要先实现 Deref 特性,因为 DerefMut 继承了 Derefpub trait DerefMut: Deref
  • 第 30 行,实参 s 的类型为 &mut MyBox<String> ,形参的类型为 &mut String,Rust 隐式地完成了转换

使用 Drop Trait 运行清理代码

智能指针类型第二个重要的 trait 是 Drop,其允许我们在智能指针的变量离开作用域时执行一些代码,通常用于释放资源。

Drop trait 要求实现一个叫做 drop 的方法,它获取一个 self 的可变引用。为了能够看出 Rust 何时调用 drop,让我们暂时使用 println! 语句实现 drop

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

当运行这个程序,会出现如下输出:

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

当实例离开作用域 Rust 会自动调用 drop,并调用我们指定的代码。变量以被创建时相反的顺序被丢弃,所以 dc 之前被丢弃。

大多数情况下不需要我们手动为类型实现 Drop trait,系统默认会对类型中的内部类型 (例如结构体的每个字段) 调用 drop。一般只用于需要释放外部资源的场景,这些外部资源是指编译器无法得知的被使用额外资源。

无法为类型同时实现 Drop 和 Copy trait。因为 Copy 是栈内存的按位浅拷贝,而 Drop 是为了释放堆内存及额外资源的。

通过 std::mem::drop 提早丢弃值

Rust 不允许显式调用 drop ,整个 Drop trait 存在的意义在于其是自动处理的,在变量的生命周期结束时自动调用 drop ,这会导致一个 double free 错误,因为 Rust 会尝试清理相同的值两次。

然而,有时你可能需要提早清理某个值。一个例子是当使用智能指针管理锁时,你可能希望强制运行 drop 方法来释放锁以便作用域中的其他代码可以获取锁。当我们希望在作用域结束之前就强制释放变量的话,我们应该使用的是由标准库提供的 std::mem::drop

std::mem::drop 函数不同于 Drop trait 中的 drop 方法。可以通过传递希望强制丢弃的值作为参数。std::mem::drop 位于 prelude。

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("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.

Dropping CustomSmartPointer with data `some data`! 出现在 CustomSmartPointer created.CustomSmartPointer dropped before the end of main. 之间,表明了 drop 方法被调用了并在此丢弃了 c

std::mem::drop 函数要求获得参数的所有权,通过 rust 所有权系统能确保变量不会被再次使用。

Rc<T> 引用计数智能指针

大部分情况下所有权是非常明确的:可以准确地知道哪个变量拥有某个值。然而,有些情况单个值可能会有多个所有者。例如,在图数据结构中,多个边可能指向相同的节点,而这个节点从概念上讲为所有指向它的边所拥有。节点在没有任何边指向它从而没有任何所有者之前,都不应该被清理掉。

为了启用多所有权需要显式地使用 Rust 类型 Rc<T>,其为 引用计数reference counting)的缩写。引用计数意味着记录一个值的引用数量来知晓这个值是否仍在被使用。如果某个值有零个引用,就代表没有任何有效引用并可以被清理。

注意 Rc<T> 只能用于单线程场景;如果计数不为 1,则不能通过指针修改指向的对象的值。

使用 Rc<T> 共享数据

我们希望创建两个共享第三个列表所有权的列表,其概念将会看起来如图所示:

591e88c1ad2628c8c5830b6ec02983ed.svg

图: 两个列表,bc, 共享第三个列表 a 的所有权

尝试使用 Box<T> 定义的 List 实现并不能工作,如下例所示:

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)))); // 列表a
    let b = Cons(3, Box::new(a));// 列表b
    let c = Cons(4, Box::new(a));// 列表c
}

编译会得出如下错误:

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。接着当再次尝试使用 a 创建 c 时,这不被允许,因为 a 的所有权已经被移动。

可以改变 Cons 的定义来存放一个引用,不过接着必须指定生命周期参数。通过指定生命周期参数,表明列表中的每一个元素都至少与列表本身存在的一样久。这是示例中元素与列表的情况,但并不是所有情况都如此。

相反,我们修改 List 的定义为使用 Rc<T> 代替 Box<T>,如下例所示。现在每一个 Cons 变量都包含一个值和一个指向 ListRc<T>。当创建 b 时,不同于获取 a 的所有权,这里会克隆 a 所包含的 Rc<List>,这会将引用计数从 1 增加到 2 并允许 ab 共享 Rc<List> 中数据的所有权。创建 c 时也会克隆 a,这会将引用计数从 2 增加为 3。每次调用 Rc::cloneRc<List> 中数据的引用计数都会增加,直到有零个引用之前其数据都不会被清理。

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)))));
    let b = Cons(3, Rc::clone(&a));
    let c = Cons(4, Rc::clone(&a));
}

需要使用 use 语句将 Rc<T> 引入作用域,因为它不在 prelude 中。在 main 中创建了存放 5 和 10 的列表并将其存放在 a 的新的 Rc<List> 中。接着当创建 bc 时,调用 Rc::clone 函数并传递 aRc<List> 的引用作为参数。

也可以调用 a.clone() 而不是 Rc::clone(&a),不过在这里 Rust 的习惯是使用 Rc::cloneRc::clone 的实现并不像大部分类型的 clone 实现那样对所有数据进行深拷贝。Rc::clone 只会增加引用计数,这并不会花费多少时间。

克隆 Rc<T> 会增加引用计数

让我们观察创建和丢弃 aRc<List> 的引用时引用计数的变化。

在下例中,修改了 main 以便将列表 c 置于内部作用域中,这样就可以观察当 c 离开作用域时引用计数如何变化。

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

在程序中每个引用计数变化的点,会打印出引用计数,其值可以通过调用 Rc::strong_count 函数获得。这个函数叫做 strong_count 而不是 count 是因为 Rc<T> 也有 weak_count

这段代码会打印出:

count after creating a = 1
count after creating b = 2
count after creating c = 3
count after c goes out of scope = 2

我们能够看到 aRc<List> 的初始引用计数为 1,接着每次调用 clone,计数会增加 1。当 c 离开作用域时,计数减 1。不必像调用 Rc::clone 增加引用计数那样调用一个函数来减少计数;Drop trait 的实现当 Rc<T> 值离开作用域时自动减少引用计数。

从这个例子我们所不能看到的是,在 main 的结尾当 b 然后是 a 离开作用域时,此处计数会是 0,同时 Rc<List> 被完全清理。使用 Rc<T> 允许一个值有多个所有者,引用计数则确保只要任何所有者依然存在其值也保持有效。

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

不过可以修改数据是非常有用的!在下一部分,我们将讨论内部可变性模式和 RefCell<T> 类型,它可以与 Rc<T> 结合使用来处理不可变性的限制。

 Arc<T>

来看看在多线程场景使用 Rc<T> 会如何:

use std::rc::Rc;
use std::thread;

fn main() {
    let s = Rc::new(String::from("多线程漫游者"));
    for _ in 0..10 {
        let s = Rc::clone(&s);
        let handle = thread::spawn(move || {
           println!("{}", s)
        });
    }
}

由于我们还没有学习多线程的章节,上面的例子就特地简化了相关的实现。首先通过 thread::spawn 创建一个线程,然后使用 move 关键字把克隆出的 s 的所有权转移到线程中。

能够实现这一点,完全得益于 Rc 带来的多所有权机制,但是以上代码会报错:

error[E0277]: `Rc<String>` cannot be sent between threads safely

表面原因是 Rc<T> 不能在线程间安全的传递,实际上是因为它没有实现 Send trait,而该 trait 是恰恰是多线程间传递数据的关键,我们会在多线程章节中进行讲解。

当然,还有更深层的原因:由于 Rc<T> 需要管理引用计数,但是该计数器并没有使用任何并发原语,因此无法实现原子化的计数操作,最终会导致计数错误。为此 Rust 为我们提供的功能类似但是多线程安全的 Arc。

Arc 是 Atomic Rc 的缩写,顾名思义:原子化的 Rc<T> 智能指针。原子化是一种并发原语,我们在后续章节会进行深入讲解,这里你只要知道它能保证我们的数据能够安全的在线程间共享即可。

你可能好奇,为何不直接使用 Arc,还要画蛇添足弄一个 Rc,还有 Rust 的基本数据类型、标准库数据类型为什么不自动实现原子化操作?这样就不存在线程不安全的问题了。

原因在于原子化或者其它锁虽然可以带来的线程安全,但是都会伴随着性能损耗,而且这种性能损耗还不小。因此 Rust 把这种选择权交给你,毕竟需要线程安全的代码其实占比并不高,大部分时候我们开发的程序都在一个线程内。

Arc 和 Rc 拥有完全一样的 API,修改起来很简单:

use std::sync::Arc;
use std::thread;

fn main() {
    let s = Arc::new(String::from("多线程漫游者"));
    for _ in 0..10 {
        let s = Arc::clone(&s);
        let handle = thread::spawn(move || {
           println!("{}", s)
        });
    }
}

两者还有一点区别:Arc 和 Rc 并没有定义在同一个模块,前者通过 use std::sync::Arc 来引入,后者通过 use std::rc::Rc

Cell 和 RefCell

Rust 的编译器之严格,可以说是举世无双。特别是在所有权方面,Rust 通过严格的规则来保证所有权和借用的正确性,最终为程序的安全保驾护航。

但是严格是一把双刃剑,带来安全提升的同时,损失了灵活性。因此 Rust 提供了 Cell 和 RefCell 用于内部可变性,简而言之,可以在拥有不可变引用的同时修改目标数据,对于正常的代码实现来说,这个是不可能做到的(要么一个可变借用,要么多个不可变借用)。

Cell 和 RefCell 的思路是将数据封装在一个单元里,提供一个接口供外部访问。

内部可变性的实现是因为 Rust 使用了 unsafe 来做到这一点,但是对于使用者来说,这些都是透明的,因为这些不安全代码都被封装到了安全的 API 中。

Cell

Cell 和 RefCell 在功能上没有区别,区别在于 Cell<T> 适用于 T 实现 Copy 的情况:

use std::cell::Cell;
fn main() {
    let c = Cell::new("asdf"); //变量c不可变,但是c的内部值可变
    let one = c.get();
    c.set("qwer");      // 修改变量c的内部值
    let two = c.get();
    println!("{},{}", one, two);
}

以上代码展示了 Cell 的基本用法,有几点值得注意:

  • "asdf" 是 &str 类型,它实现了 Copy 特性
  • c.get 用来取值,c.set 用来设置新值

取到值保存在 one 变量后,还能同时进行修改,这个违背了 Rust 的借用规则,但是由于 Cell 的存在,我们很优雅地做到了这一点,但是如果你尝试在 Cell 中存放 String

let c = Cell::new(String::from("asdf"));

编译器会立刻报错,因为 String 没有实现 Copy 特性:

| pub struct String {
| ----------------- doesn't satisfy `String: Copy`
|
= note: the following trait bounds were not satisfied:
        `String: Copy`

RefCell

由于 Cell 类型针对的是实现了 Copy 特性的值类型,因此在实际开发中,Cell 使用的并不多,因为我们要解决的往往是可变、不可变引用共存导致的问题,此时就需要借助于 RefCell 来达成目的。

我们可以将所有权、借用规则与这些智能指针做一个对比:

Rust 规则智能指针带来的额外规则
一个数据只有一个所有者Rc/Arc 让一个数据可以拥有多个所有者
要么多个不可变借用,要么一个可变借用RefCell 实现编译期可变、不可变引用共存
违背规则导致编译错误违背规则导致运行时 panic

可以看出,Rc/Arc 和 RefCell 合在一起,解决了 Rust 中严苛的所有权和借用规则带来的某些场景下难使用的问题。但是它们并不是银弹,例如 RefCell 实际上并没有解决可变引用和引用可以共存的问题,只是将报错从编译期推迟到运行时,从编译器错误变成了 panic 异常

use std::cell::RefCell;

fn main() {
    let s = RefCell::new(String::from("hello, world"));
    let s1 = s.borrow();
    let s2 = s.borrow_mut();

    println!("{},{}", s1, s2);
}

上面代码在编译期不会报任何错误,可以运行程序:

thread 'main' panicked at 'already borrowed: BorrowMutError', src/main.rs:6:16
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

但是依然会因为违背了借用规则导致了运行期 panic

相信有读者有疑问,这么做有任何意义吗?还不如在编译期报错,至少能提前发现问题,而且性能还更好。

存在即合理,究其根因,在于 Rust 编译期的宁可错杀,绝不放过的原则,当编译器不能确定你的代码是否正确时,就统统会判定为错误,因此难免会导致一些误报。

而 RefCell 正是用于你确信代码是正确的,而编译器却发生了误判时

对于大型的复杂程序,也可以选择使用 RefCell 来让事情简化。例如数据需要被分散在各个代码片段广泛使用或修改,由于这种分散在各处的使用方式,导致了管理可变和不可变成为一件非常复杂的任务(甚至不可能),你很容易就碰到编译器抛出来的各种错误。而且 RefCell 的运行时错误在这种情况下也变得非常可爱:一旦有人做了不正确的使用,代码会 panic,然后告诉我们哪些借用冲突了。

总之,当你确信编译器误报但不知道该如何解决时,或者你有一个引用类型,需要被四处使用和修改然后导致借用关系难以管理时,都可以优先考虑使用 RefCell

RefCell 简单总结:

  • 与 Cell 用于可 Copy 的值不同,RefCell 用于引用
  • RefCell 只是将借用规则从编译期推迟到程序运行期,并不能帮你绕过这个规则
  • RefCell 适用于编译期误报或者一个引用被在多处代码使用、修改以至于难于管理借用关系时
  • Cell 不会 panic,而使用 RefCell 时,违背借用规则会导致运行期的 panic

总之,当非要使用内部可变性时,首选 Cell,只有你的类型没有实现 Copy 时,才去选择 RefCell

Rc + RefCell 组合使用

在 Rust 中,一个常见的组合就是 Rc 和 RefCell 在一起使用,前者可以实现一个数据拥有多个所有者,后者可以实现数据的可变性:

use std::cell::RefCell;
use std::rc::Rc;
fn main() {
    let s = Rc::new(RefCell::new("I am mutable, and has multiple owner".to_string()));

    let s1 = s.clone();
    let s2 = s.clone();
    // let mut s2 = s.borrow_mut();
    s2.borrow_mut().push_str(", oh yeah!");

    println!("{:?}\n{:?}\n{:?}", s, s1, s2);
}

上面代码中,我们使用 RefCell<String> 包裹一个字符串,同时通过 Rc 创建了它的三个所有者:ss1s2,并且通过其中一个所有者 s2 对字符串内容进行了修改。

由于 Rc 的所有者们共享同一个底层的数据,因此当一个所有者修改了数据时,会导致全部所有者持有的数据都发生了变化。

程序的运行结果也在预料之中:

RefCell { value: "I am mutable, and has multiple owner, oh yeah!" }
RefCell { value: "I am mutable, and has multiple owner, oh yeah!" }
RefCell { value: "I am mutable, and has multiple owner, oh yeah!" }

通过 Cell::from_mut 解决借用冲突

在 Rust 1.37 版本中新增了两个非常实用的方法:

  • Cell::from_mut,该方法将 &mut T 转为 &Cell<T>
  • Cell::as_slice_of_cells,该方法将 &Cell<[T]> 转为 &[Cell<T>]

看看如何使用这两个方法来解决一个常见的借用冲突问题:

fn is_even(i: i32) -> bool {
    i % 2 == 0
}

fn retain_even(nums: &mut Vec<i32>) {
    let mut i = 0;
    for num in nums.iter().filter(|&num| is_even(*num)) {
        nums[i] = *num;
        i += 1;
    }
    nums.truncate(i);
}

以上代码会报错:

error[E0502]: cannot borrow `*nums` as mutable because it is also borrowed as immutable
 --> src/main.rs:8:9
  |
7 |     for num in nums.iter().filter(|&num| is_even(*num)) {
  |                  ----------------------------------------
  |                |
  |                immutable borrow occurs here
  |                immutable borrow later used here
8 |         nums[i] = *num;
  |         ^^^^ mutable borrow occurs here

很明显,报错是因为同时借用了不可变与可变引用,这时就可以使用 Cell 新增的这两个方法:

use std::cell::Cell;

fn retain_even(nums: &mut Vec<i32>) {
    let slice: &[Cell<i32>] = Cell::from_mut(&mut nums[..])
        .as_slice_of_cells();

    let mut i = 0;
    for num in slice.iter().filter(|num| is_even(num.get())) {
        slice[i].set(num.get());
        i += 1;
    }

    nums.truncate(i);
}

此时代码将不会报错,因为 Cell 上的 set 方法获取的是不可变引用 pub fn set(&self, val: T)

当然,以上代码的本质还是对 Cell 的运用,只不过这两个方法可以很方便的帮我们把 &mut [T] 类型转换成 &[Cell<T>] 类型。

引用循环与内存泄漏

Rust 的内存安全性保证使其难以意外地制造永远也不会被清理的内存(被称为 内存泄漏memory leak)),但并不是不可能。

制造引用循环

让我们看看引用循环是如何发生的以及如何避免它。先来看 List 枚举和 tail 方法的定义开始:

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

Cons 成员的第二个元素是 RefCell<Rc<List>>,这意味着够修改 Cons 成员所指向的 List。这里还增加了一个 tail 方法来方便我们在有 Cons 成员的时候访问其第二项。

在下例中增加了一个 main 函数,在 a 中创建了一个列表,一个指向 a 中列表的 b 列表,接着修改 a 中的列表指向 b 中的列表,这会创建一个引用循环。在这个过程的多个位置有 println! 语句展示引用计数。

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

这里在变量 a 中创建了一个 Rc<List> 实例来存放初值为 5, NilList 值。接着在变量 b 中创建了存放包含值 10 和指向列表 aList 的另一个 Rc<List> 实例。

最后,修改 a 使其指向 b 而不是 Nil,这就创建了一个循环。为此需要使用 tail 方法获取 aRefCell<Rc<List>> 的引用,并放入变量 link 中。接着使用 RefCell<Rc<List>>borrow_mut 方法将其值从存放 NilRc<List> 修改为 b 中的 Rc<List>

如果保持最后的 println! 行注释并运行代码,会得到如下输出:

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

可以看到将列表 a 修改为指向 b 之后, ab 中的 Rc<List> 实例的引用计数都是 2。在 main 的结尾,Rust 丢弃 b,这会使 b Rc<List> 实例的引用计数从 2 减为 1。然而,b Rc<List> 不能被回收,因为其引用计数是 1 而不是 0。接下来 Rust 会丢弃 aa Rc<List> 实例的引用计数从 2 减为 1。这个实例也不能被回收,因为 b Rc<List> 实例依然引用它,所以其引用计数是 1。这些列表的内存将永远保持未被回收的状态。为了更形象的展示,我们创建了一个如下图所示的引用循环:

cf729124fff795c2d397f6d1bfb8f9c9.svg

图: 列表 a 和 b 彼此互相指向形成引用循环

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

相比真实世界的程序,这个例子中创建引用循环的结果并不可怕。创建了引用循环之后程序立刻就结束了。如果在更为复杂的程序中并在循环里分配了很多内存并占有很长时间,这个程序会使用多于它所需要的内存,并有可能压垮系统并造成没有内存可供使用。

创建引用循环并不容易,但也不是不可能。如果你有包含 Rc<T>RefCell<T> 值或类似的嵌套结合了内部可变性和引用计数的类型,请务必小心确保你没有形成一个引用循环;你无法指望 Rust 帮你捕获它们。创建引用循环是一个程序上的逻辑 bug,你应该使用自动化测试、代码评审和其他软件开发最佳实践来使其最小化。

另一个解决方案是重新组织数据结构,使得一部分引用拥有所有权而另一部分没有。换句话说,循环将由一些拥有所有权的关系和一些无所有权的关系组成,只有所有权关系才能影响值是否可以被丢弃。

让我们看看一个由父节点和子节点构成的图的例子,观察何时是使用无所有权的关系来避免引用循环的合适时机。

避免引用循环:将 Rc<T> 变为 Weak<T>

到目前为止,我们已经展示了调用 Rc::clone 会增加 Rc<T> 实例的 strong_count,和只在其 strong_count 为 0 时才会被清理的 Rc<T> 实例。你也可以通过调用 Rc::downgrade 并传递 Rc<T> 实例的引用来创建其值的 弱引用weak reference)。强引用代表如何共享 Rc<T> 实例的所有权。弱引用并不属于所有权关系,当 Rc<T> 实例被清理时其计数没有影响。它们不会造成引用循环,因为任何涉及弱引用的循环会在其相关的值的强引用计数为 0 时被打断。

调用 Rc::downgrade 时会得到 Weak<T> 类型的智能指针。不同于将 Rc<T> 实例的 strong_count 加 1,调用 Rc::downgrade 会将 weak_count 加 1。Rc<T> 类型使用 weak_count 来记录其存在多少个 Weak<T> 引用,类似于 strong_count其区别在于 weak_count 无需计数为 0 就能使 Rc<T> 实例被清理。

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

因为 Weak<T> 引用的值可能已经被丢弃了,为了使用 Weak<T> 所指向的值,我们必须确保其值仍然有效。为此可以调用 Weak<T> 实例的 upgrade 方法,这会返回 Option<Rc<T>>。如果 Rc<T> 值还未被丢弃,则结果是 Some;如果 Rc<T> 已被丢弃,则结果是 None。因为 upgrade 返回一个 Option<Rc<T>>,Rust 会确保处理 SomeNone 的情况,所以它不会返回非法指针。

我们会创建一个某项知道其子项和父项的树形结构的例子,而不是只知道其下一项的列表。

创建树形数据结构:带有子节点的 Node

在最开始,我们将会构建一个带有子节点的树。让我们创建一个用于存放其拥有所有权的 i32 值和其子节点引用的 Node

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> 的项的类型被定义为 Rc<Node>。我们还希望能修改其他节点的子节点,所以 childrenVec<Rc<Node>> 被放进了 RefCell<T>

接下来,使用此结构体定义来创建一个叫做 leaf 的带有值 3 且没有子节点的 Node 实例,和另一个带有值 5 并以 leaf 作为子节点的实例 branch,如下例所示:

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

这里克隆了 leaf 中的 Rc<Node> 并储存在 branch 中,这意味着 leaf 中的 Node 现在有两个所有者:leafbranch。可以通过 branch.childrenbranch 中获得 leaf,不过无法从 leafbranchleaf 没有到 branch 的引用且并不知道它们相互关联。我们希望 leaf 知道 branch 是其父节点。稍后我们会这么做。

增加从子到父的引用

为了使子节点知道其父节点,需要在 Node 结构体定义中增加一个 parent 字段。问题是 parent 的类型应该是什么。我们知道其不能包含 Rc<T>,因为这样 leaf.parent 将会指向 branchbranch.children 会包含 leaf 的指针,这会形成引用循环,会造成其 strong_count 永远也不会为 0。

现在换一种方式思考这个关系,父节点应该拥有其子节点:如果父节点被丢弃了,其子节点也应该被丢弃。然而子节点不应该拥有其父节点:如果丢弃子节点,其父节点应该依然存在。这正是弱引用的例子!

所以 parent 使用 Weak<T> 类型而不是 Rc<T>,具体来说是 RefCell<Weak<Node>>。现在 Node 结构体定义看起来像这样:

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

这样,一个节点就能够引用其父节点,但不拥有其父节点。在下例中,我们更新 main 来使用新定义以便 leaf 节点可以通过 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 开始时没有父节点,所以我们新建了一个空的 Weak 引用实例。

此时,当尝试使用 upgrade 方法获取 leaf 的父节点引用时,会得到一个 None 值。如第一个 println! 输出所示:

leaf parent = None

当创建 branch 节点时,其也会新建一个 Weak<Node> 引用,因为 branch 并没有父节点。leaf 仍然作为 branch 的一个子节点。一旦在 branch 中有了 Node 实例,就可以修改 leaf 使其拥有指向父节点的 Weak<Node> 引用。这里使用了 leafparent 字段里的 RefCell<Weak<Node>>borrow_mut 方法,接着使用了 Rc::downgrade 函数来从 branch 中的 Rc<Node> 值创建了一个指向 branchWeak<Node> 引用。

当再次打印出 leaf 的父节点时,这一次将会得到存放了 branchSome 值:现在 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_countRc::weak_count 调用的结果看出。

可视化 strong_countweak_count 的改变

让我们通过创建了一个新的内部作用域并将 branch 的创建放入其中,来观察 Rc<Node> 实例的 strong_countweak_count 值的变化。这会展示当 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 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,其 Rc<Node> 的强引用计数为 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> 唯一的引用了。

所有这些管理计数和值的逻辑都内建于 Rc<T>Weak<T> 以及它们的 Drop trait 实现中。通过在 Node 定义中指定从子节点到父节点的关系为一个 Weak<T> 引用,就能够拥有父节点和子节点之间的双向引用而不会造成引用循环和内存泄漏。

总结

这一章涵盖了如何使用智能指针来做出不同于 Rust 常规引用默认所提供的保证与取舍。Box<T> 有一个已知的大小并指向分配在堆上的数据。Rc<T> 记录了堆上数据的引用数量以便可以拥有多个所有者。RefCell<T> 和其内部可变性提供了一个可以用于当需要不可变类型但是需要改变其内部值能力的类型,并在运行时而不是编译时检查借用规则。

我们还介绍了提供了很多智能指针功能的 trait DerefDrop。同时探索了会造成内存泄漏的引用循环,以及如何使用 Weak<T> 来避免它们。