RefCell<T>和 Interior Mutability 模式

内部可变性是 Rust 中的一种设计模式,允许你进行 mut data 即使存在对该数据的不可变引用;通常,这个 作 不允许借用规则。为了改变数据,该模式使用unsafe代码来改变 Rust 的通常规则 突变和借用。不安全代码向编译器表明我们是 手动检查规则,而不是依赖编译器来检查它们 对我们来说;我们将在第 19 章中更多地讨论不安全代码。

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

让我们通过查看RefCell<T>类型,该类型跟在 内部可变性模式。

在运行时实施借用规则RefCell<T>

Rc<T>RefCell<T>type 表示对数据的单一所有权 它成立。那么,是什么造就了RefCell<T>与像Box<T>? 回想一下你在第 4 章中学到的借用规则:

  • 在任何给定时间,您都可以拥有一个(但不能同时拥有)一个可变引用 或任意数量的不可变引用。
  • 引用必须始终有效。

使用引用和Box<T>,借用规则的不变量在 编译时。跟RefCell<T>,则这些不变量将在运行时强制执行。 对于引用,如果你违反了这些规则,你将得到一个编译器错误。跟RefCell<T>,如果违反这些规则,程序将 panic 并退出。

在编译时检查借用规则的优点是 error 会在开发过程中更早地捕获,并且不会对 运行时性能,因为所有分析都是事先完成的。对于那些 原因,在编译时检查借用规则是 大多数情况下,这就是为什么这是 Rust 的默认。

相反,在运行时检查借用规则的优点是 然后,允许某些内存安全方案,它们本应是 被编译时检查不允许。静态分析,如 Rust 编译器, 本质上是保守的。代码的某些属性无法通过 分析代码:最著名的例子是 Halting Problem,它是 超出了本书的范围,但这是一个有趣的研究话题。

因为某些分析是不可能的,如果 Rust 编译器无法确定 code 符合所有权规则,它可能会拒绝正确的程序;在 这样,它是保守的。如果 Rust 接受了不正确的程序,则用户 将无法信任 Rust 所做的保证。但是,如果 Rust 拒绝正确的程序,程序员会感到不便,但什么都没有 灾难性可能会发生。这RefCell<T>type 在你确定你的 代码遵循借用规则,但编译器无法理解和 保证这一点。

Rc<T>,RefCell<T>仅用于单线程方案 如果您尝试在多线程中使用它,则会出现编译时错误 上下文。我们将讨论如何获取RefCell<T>在 multithreaded program 的命令。

以下是选择的原因Box<T>,Rc<T>RefCell<T>:

  • Rc<T>支持同一数据的多个所有者;Box<T>RefCell<T>拥有单一所有者。
  • Box<T>允许在编译时检查不可变或可变借用;Rc<T>只允许在编译时检查不可变的借款;RefCell<T>允许 在运行时检查 Immutable 或 Mutable Borrows。
  • 因为RefCell<T>允许在运行时检查可变借用,您可以 mutate 中的 valueRefCell<T>即使RefCell<T>是 变。

改变不可变值中的值是内部可变性模式。让我们看看内部可变性有用的情况,并且 检查它是如何可能的。

内部可变性:对不可变值的 Mutable Borrow

借用规则的结果是,当你有一个不可变的值时, 你不能可变地借用它。例如,此代码不会编译:

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

如果您尝试编译此代码,则会收到以下错误:

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

For more information about this error, try `rustc --explain E0596`.
error: could not compile `borrowing` (bin "borrowing") due to 1 previous error

但是,在某些情况下,值发生 mutate 会很有用 本身,但对其他代码似乎不可变。代码 value 的方法将无法改变该值。用RefCell<T>是 一种方法是获得具有内部可变性的能力,但RefCell<T>并不能完全绕过借用规则:在 compiler 允许这种内部可变性,并且会检查借用规则 而是在 runtime 中。如果您违反规则,您将收到panic!而不是 编译器错误。

让我们通过一个实际示例来使用RefCell<T>以 mutate 一个不可变的值,并了解为什么它很有用。

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

有时在测试过程中,程序员会使用一种类型来代替另一种类型。 为了观察特定行为并断言它被正确实现。 此占位符类型称为 test double。从 a 电影制作中的“特技替身”,一个人介入并替代 actor 来做一个特别棘手的场景。测试替身替代其他类型 当我们运行测试时。Mock 对象是特定类型的测试替身 记录测试期间发生的情况,以便您可以断言正确的 行动发生了。

Rust 没有像其他语言那样有对象, 并且 Rust 没有内置到标准库中的 mock 对象功能 就像其他一些语言一样。但是,您绝对可以创建一个结构体 将用于与 mock 对象相同的目的。

以下是我们将测试的场景:我们将创建一个跟踪值的库 根据最大值发送消息,并根据接近最大值的程度发送消息 value 的 current value 为这个库可用于跟踪 例如,用户允许进行的 API 调用次数的配额。

我们的库将仅提供跟踪与 maximum a value is 以及消息在什么时间应该是什么。应用 将需要提供发送 messages:应用程序可以在应用程序中放置一条消息,发送一个 电子邮件、发送短信或其他内容。库不需要知道 那个细节。它所需要的只是实现我们将提供的 trait 的东西 叫Messenger.示例 15-20 显示了库代码:

文件名: src/lib.rs

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

示例 15-20:一个用于跟踪 value 的值为最大值,并在该值处于特定级别时发出警告

此代码的一个重要部分是Messengertrait 只有一个方法 叫send它接受对self和 消息。这个 trait 是我们的 mock 对象需要实现的接口,以便 mock 可以像真实对象一样使用。另一个重要部分 是我们希望测试set_value方法上的LimitTracker.我们可以更改传入的valueparameter 的set_value不会返回任何内容供我们进行断言。我们希望成为 可以说,如果我们创建一个LimitTracker与实现 这Messengertrait 和max,当我们传递不同的 的 numbersvalue,则指示 Messenger 发送相应的消息。

我们需要一个 mock 对象,而不是发送电子邮件或短信 叫send,将只跟踪它被告知发送的消息。我们可以 创建 mock 对象的新实例,创建一个LimitTracker,它使用 mock 对象中,调用set_valuemethod 开启LimitTracker,然后检查 mock 对象包含我们期望的消息。示例 15-21 显示了 实现一个 mock 对象来做到这一点,但 Borrow 检查器不允许这样做:

文件名: src/lib.rs

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

#[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这是 Borrow Checker 不允许的

此测试代码定义了一个MockMessenger结构体具有sent_messages字段,其中VecString值来跟踪它被告知的消息 以发送。我们还定义了一个关联的函数new为了方便 新建MockMessenger以空消息列表开头的值。我们 然后实现Messenger的 traitMockMessenger所以我们可以给出一个MockMessenger更改为LimitTracker.在send方法,我们 将传入的消息作为参数存储在MockMessenger列表sent_messages.

在测试中,我们将测试当LimitTracker被告知设置value更改为超过 75% 的max价值。首先,我们 新建MockMessenger,它将以空消息列表开头。 然后我们创建一个新的LimitTracker并为其提供对新MockMessenger以及max值为 100。我们调用set_value方法上的LimitTracker值为 80,即 100 的 75% 以上。然后 我们断言,MockMessenger正在跟踪 of 现在应该包含一条消息。

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

$ cargo test
   Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
error[E0596]: cannot borrow `self.sent_messages` as mutable, as it is behind a `&` reference
  --> src/lib.rs:58:13
   |
58 |             self.sent_messages.push(String::from(message));
   |             ^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be borrowed as mutable
   |
help: consider changing this to be a mutable reference in the `impl` method and the `trait` definition
   |
2  ~     fn send(&mut self, msg: &str);
3  | }
...
56 |     impl Messenger for MockMessenger {
57 ~         fn send(&mut self, message: &str) {
   |

For more information about this error, try `rustc --explain E0596`.
error: could not compile `limit-tracker` (lib test) due to 1 previous error

我们无法修改MockMessenger来跟踪消息,因为sendmethod 接受对self.我们也不能接受 错误文本中要使用的建议&mut self相反,因为这样 签名sendMessenger特性 定义(请随意尝试并查看您收到的错误信息)。

在这种情况下,内部可变性可以提供帮助!我们将存储sent_messagesRefCell<T>,然后是sendmethod 将为 能够修改sent_messages来存储我们看到的消息。示例 15-22 显示了它是什么样子的:

文件名: src/lib.rs

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

#[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() {
        // --snip--
        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);
    }
}

示例 15-22:使用RefCell<T>要更改 Inner value 的值,而外部值被认为是不可变的

sent_messagesfield 现在为RefCell<Vec<String>>而不是Vec<String>.在new函数,我们会创建一个新的RefCell<Vec<String>>实例。

为了实现send方法,则第一个参数仍然是 的 immutable borrowself,这与特征定义匹配。我们调用borrow_mutRefCell<Vec<String>>self.sent_messages要获取 mutable 引用添加到RefCell<Vec<String>>,即 向量。然后我们可以调用push在对 vector 的可变引用上保留 track 测试期间发送的消息。

我们要做的最后一个更改是在 assert 中:查看有多少项是 在内部向量中,我们调用borrowRefCell<Vec<String>>要获取 对 vector 的不可变引用。

现在,您已经了解了如何使用RefCell<T>,让我们深入了解它是如何工作的!

在运行时跟踪借款RefCell<T>

在创建不可变和可变引用时,我们使用 和&&mut语法。跟RefCell<T>,我们使用borrowborrow_mut方法,这些 API 是属于RefCell<T>.这borrowmethod 返回智能指针类型Ref<T>borrow_mut返回智能指针类型RefMut<T>.两种类型都实现Deref,所以我们 可以将它们视为常规引用。

RefCell<T>跟踪数量Ref<T>RefMut<T>聪明 指针当前处于活动状态。每次我们调用borrowRefCell<T>增加其活动不可变借用数的计数。当Ref<T>value 超出范围,则不可变借用的计数将减少 1。只 就像编译时借用规则一样,RefCell<T>让我们有许多不可变的 borrows 或一个可变 borrow。

如果我们试图违反这些规则,而不是像 将与参考一起,实现RefCell<T>会惊慌 运行。示例 15-23 显示了对send在 示例 15-22.我们特意尝试创建两个 active 的可变借用 对于相同的范围,以说明RefCell<T>阻止我们这样做 在运行时。

文件名: src/lib.rs

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

#[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) {
            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));
        }
    }

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

示例 15-23:在 相同的范围来查看RefCell<T>会恐慌

我们创建一个变量one_borrow对于RefMut<T>返回智能指针 从borrow_mut.然后我们以相同的方式在 变量two_borrow.这会在同一范围内生成两个可变引用 这是不允许的。当我们为我们的库运行测试时,清单中的代码 15-23 将编译时没有任何错误,但测试将失败:

$ cargo test
   Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.91s
     Running unittests src/lib.rs (target/debug/deps/limit_tracker-e599811fa246dbde)

running 1 test
test tests::it_sends_an_over_75_percent_warning_message ... FAILED

failures:

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


failures:
    tests::it_sends_an_over_75_percent_warning_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`

请注意,代码 paniced 并显示消息already borrowed: BorrowMutError.就是这样RefCell<T>处理借用违规 规则。

选择在运行时而不是编译时捕获借用错误,因为 我们在这里完成了,这意味着您稍后可能会在代码中发现错误 在开发过程中:可能直到您的代码部署到 生产。此外,您的代码将产生较小的运行时性能损失,因为 这是在运行时而不是编译时跟踪借用的结果。 但是,使用RefCell<T>可以编写一个 mock 对象,该对象可以 修改自身以跟踪您在使用它时看到的消息 在只允许不可变值的上下文中。您可以使用RefCell<T>尽管它需要权衡取舍以获得比常规引用更多的功能 提供。

通过组合拥有可变数据的多个所有者Rc<T>RefCell<T>

一种常见的使用RefCell<T>Rc<T>.回想一下Rc<T>允许您拥有某些数据的多个所有者,但它只提供 Immutable 访问该数据。如果你有Rc<T>,它持有一个RefCell<T>您可以 获取一个可以具有多个所有者并且可以更改的值!

例如,回想一下示例 15-18 中的 cons list 示例,我们在那里使用了Rc<T>以允许多个列表共享另一个列表的所有权。因为Rc<T>只保存不可变值,我们不能更改 list 创建它们后。让我们添加RefCell<T>要获得 更改列表中的值。示例 15-24 显示,通过使用RefCell<T>Cons定义,我们可以修改存储在 ALL 名单:

文件名: src/main.rs

#[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:?}");
}

示例 15-24:使用Rc<RefCell<i32>>要创建一个List我们可以变异

我们创建一个值,它是Rc<RefCell<i32>>并将其存储在 名为value这样我们就可以稍后直接访问它了。然后我们创建一个Lista替换为Cons变体,该value.我们需要克隆value所以两者都avalue拥有内部5value 而不是 而不是转移所有权valuea或具有a借款自value.

我们包装列表aRc<T>因此,当我们创建列表时bc他们 都可以指a,这就是我们在示例 15-18 中所做的。

在 中创建列表后a,bc,我们想将 10 添加到 值value.我们通过调用borrow_mutvalue,它使用 我们在第 5 章中讨论的自动取消引用功能(参见“作员在哪里?”->) 更改为 取消引用Rc<T>到内部RefCell<T>价值。这borrow_mutmethod 返回一个RefMut<T>智能指针,我们使用 dereference 运算符 并更改内部值。

当我们打印时a,bc,我们可以看到它们都修改了 值为 15 而不是 5:

$ cargo run
   Compiling cons-list v0.1.0 (file:///projects/cons-list)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.63s
     Running `target/debug/cons-list`
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))

这个技术很巧妙!通过使用RefCell<T>,我们有一个 变List价值。但是我们可以在RefCell<T>提供 访问其内部可变性,以便我们可以在需要时修改数据。 借用规则的运行时检查保护我们免受数据竞争的影响,它是 有时值得以速度换取我们数据的灵活性 结构。请注意,RefCell<T>不适用于多线程代码!Mutex<T>是 的线程安全版本RefCell<T>我们将讨论Mutex<T>在第 16 章中。

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