RefCell<T> 与内部可变性模式

RefCell<T> and the Interior Mutability Pattern

内部可变性,interior mutability 属于 Rust 中的一种设计模式,他实现了即使在有着到数据的一些不可变引用之下,对数据加以改变;一般情况下,这样的行为是借用规则所不允许的。为了改变数据,这种模式便运用了数据结构内部的一些 unsafe 代码,来改变了 Rust 监管可变性与借用的一些一般规则。这些不安全代码向编译器表明,咱们自己在手动检查那些规则,而非依赖于编译器为咱们检查那些规则;在第 19 章将进一步讨论这些不安全代码。

只有当我们可以确保在运行时遵循借用规则时,我们才能使用使用内部可变性模式的类型,即使编译器不能保证这一点。然后将涉及的不安全代码包装在安全的 API 中,并且外部类型仍然是不可变的。

咱们来通过检视遵循内部可变性模式的 RefCell<T> 类型来探讨这个概念。

使用 RefCell<T> 在运行时执行借用规则检查

Enforcing Borrowing Rules at Runtime with RefCell<T>

Rc<T> 不同,RefCell<T> 类型表示对其所持有的数据的单一所有权。那么,是什么使 RefCell<T>Box<T> 这样的类型不同呢?回顾咱们在第四章学到的借用规则:

  • 在任何给定时间,咱们都可以有着 要么 (而非同时) 一个可变引用,要么任意数量的不可变引用;
  • 引用必须始终有效。

对于引用与 Box<T>,借用规则的不变性,the borrowing rules' invariants, 是在编译时强制执行的。对于 RefCell<T>,这些不变性则是在运行时强制执行的。对于引用,如果咱们破坏了这些规则,咱们会得到编译器报错。而在 RefCell<T> 中,如果咱们破坏了这些规则,咱们的程序将终止运行。

在编译时检查借用规则的好处是在开发过程中会更早地发现错误,而且对运行时性能没有影响,因为所有分析都是事先完成的。由于这些原因,在大多数情况下,在编译时检查借用规则是最好的选择,这就是为什么这是 Rust 的默认设置。

相反,在运行时检查借用规则的优点是允许某些内存安全的场景,而编译时检查则不会允许这些场景。与 Rust 编译器一样,静态分析,static analysis,本质上是保守的。代码的某些属性无法通过分析代码来检测:最著名的例子是停机问题,the Halting Problem, 它超出了本书的范围,但却是一个值得研究的有趣主题。

由于某些分析是不可行的,那么如果 Rust 编译器不能确定代码符合所有权规则,他可能会拒绝某个正确的程序;从这方面讲,他是保守的。如果 Rust 编译器接受了错误的程序,用户就无法相信 Rust 做出的保证。然而,如果 Rust 拒绝了某个正确的程序,编程者会感到不便,但又不会发生什么灾难性的事情。在咱们确定咱们的代码遵循借用规则,而编译器无法理解和保证时,RefCell<T> 类型就很有用。

Rc<T> 类似,RefCell<T> 只适用于单线程场景,如果咱们试图在多线程环境下使用它,会出现编译时错误。我们将在第 16 章讨论如何在多线程程序中获得 RefCell<T> 的功能。

下面是对选择 Box<T>Rc<T>RefCell<T> 理由的总结:

  • Rc<T> 使同一数据有多个所有者;Box<T>RefCell<T> 有单一所有者;
  • Box<T> 允许在编译时检查不可变或可变的借用;Rc<T> 只允许在编译时检查不可变的借用;RefCell<T> 允许在运行时检查不可变或可变的借用;
  • 因为 RefCell<T> 允许在运行时检查可变的借用,所以即使 RefCell<T> 是不可变的,咱们也可以改变 RefCell<T> 中的值。

改变不可变值内部的值,就是 内部可变性模式,the interior mutablity pattern。让我们看一下内部可变性有用的一种情况,并检视其如何可行。

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

Interior Mutability: A Mutable Borrow to an Immutable Value

借用规则的一种后果是,当咱们有一个不可变的值时,咱们不能以可变方式借用他。比如,下面这段代码就不能编译:

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

如果咱们试图编译这段代码,咱们会得到以下错误:

$ cargo run
   Compiling sp_demos v0.1.0 (/home/lennyp/rust-lang/sp_demos)
error[E0596]: cannot borrow `x` as mutable, as it is not declared as mutable
 --> src/main.rs:3:13
  |
2 |     let x = 5;
  |         - help: consider changing this to be mutable: `mut x`
3 |     let y = &mut x;
  |             ^^^^^^ cannot borrow as mutable

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

然而,在有些情况下,值在其方法中改变自身,但对其他代码来说却显得不可改变,这将是非常有用的。在该值的方法之外的代码将不能改变该值。使用 RefCell<T> 是获得内部可变性能力的一种方法,但是 RefCell<T> 并没有完全绕过借用规则:编译器中的借用检查器会放行这种内部可变性,而代之以在运行时借用规则得以检查。如果咱们违反了这些规则,咱们会得到一个 pani! 而不是一个编译器报错。

咱们来通过一个其中咱们可以使用 RefCell<T> 改变一个不可变的值的实际示例,看看为什么这很有用。

内部可变性的用例:模拟对象

A Use Case for Interior Mutability: Mock Objects

有时在测试过程中,程序员会使用一个类型来代替另一类型,以便观察特定的行为并断定其实现是正确的。这种占位符类型被称为 测试替身,test double。请从电影制作中的“特技替身,stunt double”的角度来考虑他,某人代替一名演员来完成特别棘手的一场戏。当咱们运行测试时,测试替身代表其他类型。模拟对象,mock objects 是特定类型的测试替身,他记录了测试过程中发生的事情,因此咱们可以断言发生了正确的动作。

Rust 没有像其他语言那样拥有对象,Rust 也没有像其他一些语言那样在标准库中内置模拟对象功能。但是,咱们绝对可以创建一个与模拟对象具有相同用途的结构。

下面是我们要测试的情景:我们将创建一个库,跟踪某个数值与最大值的关系,并根据当前数值与最大值的接近程度发送消息。例如,这个库可以用来跟踪用户允许调用的 API 数量配额。

这个库将提供跟踪某个值接近最大值的程度,及在什么时刻发出什么消息的功能。使用这个库的应用,将被期望提供发送消息的机制:应用可以在应用中放置消息、发送电子邮件、发出手机短信或其他东西。库不需要知道这个细节。他所需的只是实现了咱们将提供的名为 Messenger 特质的东西。以下清单 15-20 给出库的代码:

文件名:src/lib.rs

#![allow(unused)]
fn main() {
{{#include ../projects/limit_tracker/src/lib.rs::35}}
}

清单 15-20:跟踪某个值与最大值接近程度,并在值处于不同水平时发出告警的库

这段代码的一个重要部分是 Messenger 特质有个叫做 send 的方法,其接收一个不可变 self 的引用和消息文本。这个特质是咱们模拟对象需要实现的接口,这样模拟对象就可以和真实对象一样被使用。另一个重要的部分是,我们要测试 LimitTrackerset_value 方法的行为。我们可以改变我们传入的 value 参数,但 set_value 并没有返回任何东西让我们做断言。我们希望能够表达出,若咱们用实现了 Messenger 特质的东西,与 max 的一个特定值创建了一个 LimitTracker,当我们为 value 传递不同的数字时,messenger 就会被告知要发送相应的消息。

注:the interface, 借鉴了 Java 语言的叫法,参见 使用接口来拯救!。而这种内部可变性模式用到的数据结构,则类似于 Java 中的内部类。

我们需要一个模拟对象,他不会在我们调用 send 时发送电子邮件或文本消息,而只会记录他被告知要发送的消息。我们可以创建模拟对象的一个新实例,创建一个使用该模拟对象的 LimitTracker 实例,调用 LimitTracker 实例的 set_value 方法,然后检查该模拟对象是否有我们期望的消息。清单 15-21 给出了一个实现模拟对象的尝试,来就这样做,但借用检查器不允许这样做:

文件名:src/lib.rs

#![allow(unused)]
fn main() {
#[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);
    }
}
}

清单 15-21:试图实现一个借用检查器不允许的 MockMessenger

这段测试代码定义了一个 MockMessenger 结构体,他有一个 send_messages 字段,里面有一个 String 值的 Vec,用来记录他被告知要发送的消息。我们还定义了一个关联函数 new,以方便创建新的 MockMessenger 值,该值以一个空的消息列表开始。然后我们为 MockMessenger 实现了 Messenger 特质,这样我们就可以给 LimitTracker 一个 MockMessenger。在 send 方法的定义中,我们将传入的消息作为参数,并将其存储在 MockMessengersend_messages 列表中。

在测试中,我们正在测试当 LimitTracker 被告知将 value 设置为超过最大值的 75% 时会发生什么。首先,我们创建一个新的 MockMessenger,他将以一个空的消息列表开始。然后我们创建一个新的 LimitTracker 并为其提供对新 MockMessenger 的引用和最大值 100。我们在 LimitTracker 上用一个大于 75% 的值 80 调用 set_value 方法。然后我们断言 MockMessenger 正在跟踪的消息列表现在应有一条消息。

但是,此测试存在一个问题,如下所示:

$ cargo test
   Compiling limit_tracker v0.1.0 (/home/lennyp/rust-lang/limit_tracker)
error[E0596]: cannot borrow `self.sent_messages` as mutable, as it is behind a `&` reference
  --> src/lib.rs:58:13
   |
2  |     fn send(&self, msg: &str);
   |             ----- help: consider changing that to be a mutable reference: `&mut self`
...
58 |             self.sent_messages.push(String::from(message));
   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be borrowed as mutable

For more information about this error, try `rustc --explain E0596`.
error: could not compile `limit_tracker` due to previous error
warning: build failed, waiting for other jobs to finish...

我们不能修改 MockMessenger 来记录消息,因为 send 方法需要一个对 self 的不可变的引用。我们也不能采纳错误文本中的建议,使用 &mut self 来代替,因为那样的话,send 的签名就无法与 Messenger 特质定义中的签名相匹配(请随意尝试,看看咱们会得到什么样的报错消息)。

这种情况下,内部可变性可以起到帮助作用!我们将把 send_messages 存储在一个 RefCell<T> 中,然后 send 方法将能够修改 send_messages 来存储我们所看到的信息。清单 15-22 显示了这是什么样子:

文件名:src/lib.rs

#![allow(unused)]
fn main() {
{{#include ../projects/limit_tracker/src/lib.rs:37:}}
}

清单 15-22:使用 RefCell<T> 来改变内层值,而外部值被认为是不可变的

sent_messages 字段现在的类型是 RefCell<Vec<String>,而不是 Vec<String>。在 new 函数中,我们围绕空向量创建一个新的 RefCell<Vec<String> 实例。

对于 send 方法的实现,第一个参数仍然是 self 的不可变借用,这与特质定义相匹配。我们对 self.send_messages 中的 RefCell<Vec<String> 调用 borrow_mut,以获得 RefCell<Vec<String> 中值的可变引用,也就是那个矢量。然后,我们可以对该矢量的可变引用调用 push,以记录测试期间发送的消息。

我们必须做的最后一个更改是在断言中:为了查看内层矢量中有多少个条目,我们在 RefCell<Vec<String>> 上调用 borrow 以获得对该矢量的不可变引用。

现在咱们已经看到了如何使用 RefCell<T>,咱们来深入了解其工作原理!

使用 RefCell<T> 在运行时记录借用

Keeping Track of Borrows at Runtime with RefCell<T>

当创建不可变和可变引用时,我们分别使用 &&mut 语法。而对于 RefCell<T>,我们使用 borrowborrow_mut 方法,他们属于 RefCell<T> 安全 API 的一部分。borrow 方法返回灵巧指针类型 Ref<T>,而 borrow_mut 返回灵巧指针类型 RefMut<T>。这两种类型都实现了 Deref,所以我们可以像对待普通引用一样对待他们。

RefCell<T> 会记录当前有多少个 Ref<T>RefMut<T> 灵巧指针是活动的。每次我们调用 borrowRefCell<T> 都会增加他的计数,即有多少个不可变借用是活动的。当一个 Ref<T> 值超出作用域时,不可变借用的计数就会减少一个。就像编译时的借用规则一样,RefCell<T> 允许我们在任何时候有许多不可变借用或一个可变的借用。

在咱们尝试违反这些规则时,与在引用下咱们会得到编译器报错不同,RefCell<T> 的实现将在运行时终止运行。下面清单 15-23 给出了清单 15-22 中那个 send 实现的修改。咱们故意为同一作用域创建了两个可变借用,以演示 RefCell<T> 在运行时阻止咱们这样做。

文件名:src/lib.rs

#![allow(unused)]
fn main() {
    impl Messenger for MockMessenger {
        fn send(&self, message: &str) {
            let mut borrow_one = self.sent_messages.borrow_mut();
            let mut borrow_two = self.sent_messages.borrow_mut();

            borrow_one.push(String::from(message));
            borrow_two.push(String::from(message));
        }
    }
}

清单 15-23:在同一作用域中创建两个可变引用,以发现 RefCell<T> 会终止运行

我们为从 borrow_mut 返回的 RefMut<T> 智能指针创建了一个变量 one_borrow。然后我们以同样的方式在变量 two_borrow 中创建了另一个可变的借用。这就在同一作用域中产生了两个可变引用,这是不允许的。当我们运行咱们库的测试时,清单 15-23 中的代码将被不带任何报错地编译,但测试将失败:

$ cargo test
   Compiling limit_tracker v0.1.0 (/home/peng/rust-lang/limit_tracker)
    Finished test [unoptimized + debuginfo] target(s) in 0.46s
     Running unittests src/lib.rs (target/debug/deps/limit_tracker-98d6159d1b15eb72)

running 1 test
test tests::it_sends_an_over_75_percent_waring_message ... FAILED

failures:

---- tests::it_sends_an_over_75_percent_waring_message stdout ----
thread 'tests::it_sends_an_over_75_percent_waring_message' panicked at 'already borrowed: BorrowMutError', src/lib.rs:60:53
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::it_sends_an_over_75_percent_waring_message

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass `--lib`

请注意该代码是以消息 already borrowed: BorrowMutError 终止运行的。这正是 RefCell<T> 处理运行时违反借用规则的方式。

选择在运行时而不是编译时捕获借用错误,正如我们在这里所做的那样,意味着咱们可能会在开发过程后期,才发现代码中的错误:可能直到咱们的代码部署到生产环境中才发现。此外,由于在运行时而不是编译时记录借用,咱们的代码会招致小的运行时性能损失。但是,使用 RefCell<T> 可以编写出模拟对象,该对象可以修改自身,来记录在咱们仅允许不可变值的上下文中使用他时,其所见到的消息。尽管 RefCell<T> 为获得比常规引用所提供的更多功能而有所取舍,咱们可以使用他。

通过结合 Rc<T>RefCell<T>,实现可变数据的多个所有者

Having Multiple Owners of Mutable Data by Combining Rc<T> and RefCell<T>

使用 RefCell<T> 的一种常见方式是与 Rc<T> 结合使用。回顾一下,Rc<T> 实现了某个数据的多个所有者,但只提供对数据的不可变访问。如果咱们有一个持有 RefCell<T>Rc<T>,咱们可以得到一个可以有着多个所有者, 咱们可以改变的值。

比如,回顾清单 15-18 中的构造列表示例,咱们使用 Rc<T> 来实现多个列表共用另一列表所有权。由于 Rc<T> 只保存不可变值,因此一旦咱们创建出列表中的任何值,咱们就再也不能改变他们。咱们来加入 RefCell<T>,以获得修改列表中值的能力。下面清单 15-24 显示,通过在 Cons 定义中使用 RefCell<T>,咱们可以修改所有列表中存储的值:

文件名:src/main.rs

#![allow(unused)]
fn main() {
{{#include ../projects/cons_list_demo/src/main.rs}}
}

清单 15-24:使用 Rc<RefCell<i32>> 创建一个咱们可改变的 List

我们创建了一个值,他是 Rc<RefCell<i32>> 的一个实例,并将其存储在一个名为 value 的变量中,以便我们稍后可以直接访问。然后我们以持有 value 的一个 Cons 变种,在 a 中创建了一个 List。我们需要克隆 value,以便 avalue 都拥有内部值 5 的所有权,而不是将所有权从 value 转移到 a 或让 avalue 借用。

我们将列表 a 包装在 Rc<T> 中,这样当我们创建列表 bc 时,他们都可以引用 a,这就是我们在示例 15-18 中所做的。

在我们创建了 abc 中的列表后,我们打算在 value 中的值上加 10。我们通过在 value 上调用 borrow_mut 来实现这一目的,他使用了我们在第 5 章中讨论过的自动解引用功能,the automatic dereferencing feature,(参见 -> 操作符去哪儿了? 小节),将 Rc<T> 解引用到内部的 RefCell<T> 值。borrow_mut 方法返回一个 RefMut<T> 灵巧指针,我们对其使用解引用操作符,并改变内部值。

在打印 abc 时,就可以看到他们都有了修改后的值 15 而非 5

$ cargo run                                                       lennyp@vm-manjaro
   Compiling cons_list_demo v0.1.0 (/home/lennyp/rust-lang/cons_list_demo)
    Finished dev [unoptimized + debuginfo] target(s) in 0.17s
     Running `target/debug/cons_list_demo`
之后的 a = Cons(RefCell { value: 15 }, Nil)
之后的 b = Cons(RefCell { value: 3 }, Cons(RefCell { value: 15 }, Nil))
之后的 c = Cons(RefCell { value: 4 }, Cons(RefCell { value: 15 }, Nil))

这个技巧非常整洁! 通过使用 RefCell<T>,我们有一个对外不可变的 List 值。但是我们可以使用 RefCell<T> 上提供对其内部可变性访问的方法,这样我们就可以在需要的时候修改我们的数据。借用规则的运行时检查可以保护我们不受数据竞赛的影响,有时值得用一点速度来换取我们数据结构中的这种灵活性。请注意,RefCell<T> 对多线程代码不起作用! Mutex<T>RefCell<T> 的线程安全版本,我们将在第 16 章讨论 Mutex<T>

Last change: 2023-12-01, commit: 71a00fa