RefCell<T> 和 Interior 可变性模式

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

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

让我们通过查看 内部可变性模式。RefCell<T>

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

与 不同 ,该类型表示对数据的单一所有权 它成立。那么,与像 ? 回想一下你在第 4 章中学到的借用规则:Rc<T>RefCell<T>RefCell<T>Box<T>

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

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

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

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

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

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

以下是选择 、 或 的原因回顾 :Box<T>Rc<T>RefCell<T>

  • Rc<T>支持同一数据的多个所有者; 并且拥有单一所有者。Box<T>RefCell<T>
  • Box<T>允许在编译时检查不可变或可变借用; 只允许在编译时检查不可变的借款; 允许 在运行时检查 Immutable 或 Mutable Borrows。Rc<T>RefCell<T>
  • 由于允许在运行时检查可变借用,因此您可以 改变 Even当 变。RefCell<T>RefCell<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 的方法将无法改变该值。使用 is 获得具有内部可变性的能力,但不能完全绕过借用规则的一种方法: compiler 允许这种内部可变性,并且会检查借用规则 而是在 runtime 中。如果您违反了规则,您将获得一个 instead 而不是 编译器错误。RefCell<T>RefCell<T>panic!

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

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

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

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

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

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

文件名: 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 的值为最大值,并在该值处于特定级别时发出警告

这段代码的一个重要部分是 trait 有一个方法 called 的 API 调用,它采用对 消息。这个 trait 是我们的 mock 对象需要实现的接口,以便 mock 可以像真实对象一样使用。另一个重要部分 是我们希望在 .我们可以更改为 parameter 传入的内容,但不返回任何内容供我们进行断言。我们希望成为 可以说,如果我们创建一个 with 实现 trait 和特定值 ,当我们传递不同的 的 numbers 中,通知 Messenger 发送相应的消息。Messengersendselfset_valueLimitTrackervalueset_valueLimitTrackerMessengermaxvalue

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

文件名: 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:尝试实现 borrow 检查器不允许的 MockMessenger

此测试代码定义了一个结构体,该结构体具有一个具有 of 值的字段,用于跟踪它所告知的消息 以发送。我们还定义了一个关联函数,以方便 创建以空消息列表开头的新值。我们 然后实现 trait for,以便我们可以将 a 赋给 a 。在方法的定义中,我们 将传入的消息作为参数,并将其存储在 的列表中。MockMessengersent_messagesVecStringnewMockMessengerMessengerMockMessengerMockMessengerLimitTrackersendMockMessengersent_messages

在测试中,我们将测试当 被告知设置为大于该值的 75% 的值时会发生什么。首先,我们 新建 ,它将以空消息列表开头。 然后我们创建一个 new 并为其提供对 new 的引用,其值为 100。我们调用 上 的方法,其值为 80,这是 100 的 75% 以上。然后 我们断言 正在跟踪的消息列表 of 现在应该包含一条消息。LimitTrackervaluemaxMockMessengerLimitTrackerMockMessengermaxset_valueLimitTrackerMockMessenger

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

$ 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

我们无法修改 the 来跟踪消息,因为该方法采用对 .我们也不能接受 建议改用错误文本,因为这样 signature 的 ID 与 trait 中的 signature 不匹配 定义(请随意尝试并查看您收到的错误信息)。MockMessengersendself&mut selfsendMessenger

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

文件名: 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> 来改变内部 value 的值,而外部值被认为是不可变的

字段现在是 type 而不是 .在该函数中,我们围绕空 vector 创建一个新实例。sent_messagesRefCell<Vec<String>>Vec<String>newRefCell<Vec<String>>

对于该方法的实现,第一个参数仍然是一个 immutable borrow 的 ,它与 trait 定义匹配。我们呼吁 in 以获得 对 中的值的可变引用,即 向量。然后我们可以调用对 vector 的可变引用来保持 track 测试期间发送的消息。sendselfborrow_mutRefCell<Vec<String>>self.sent_messagesRefCell<Vec<String>>push

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

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

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

在创建不可变和可变引用时,我们分别使用 and 语法。在 中,我们使用 和 方法,它们是属于 的安全 API 的一部分。该方法返回 智能指针类型 ,并返回智能指针类型 。这两种类型都实现了 ,因此我们 可以将它们视为常规引用。&&mutRefCell<T>borrowborrow_mutRefCell<T>borrowRef<T>borrow_mutRefMut<T>Deref

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

如果我们试图违反这些规则,而不是像 将与引用一起,的实现将 panic at 运行。示例 15-23 显示了对 in 实现的修改 示例 15-22.我们特意尝试创建两个 active 的可变借用 对于相同的范围来说明阻止我们执行此作 在运行时。RefCell<T>sendRefCell<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> 会 panic

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

$ 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`

请注意,代码出现 panic 并显示消息 .这是如何处理借款违规行为 规则。already borrowed: BorrowMutErrorRefCell<T>

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

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

一种常见的使用方式是与 结合使用。回想一下,它允许您拥有某些数据的多个所有者,但它只提供不可变的 访问该数据。如果你有一个包含 的 ,则可以 获取一个可以具有多个所有者并且可以更改的值!RefCell<T>Rc<T>Rc<T>Rc<T>RefCell<T>

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

文件名: 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

我们创建一个值,它是 的实例,并将其存储在 变量命名,以便我们稍后可以直接访问它。然后,我们创建一个 in,其中包含一个包含 的变体。我们需要克隆两者,并拥有内部值的所有权,而不是 比将所有权从 转移到 或从 借款。Rc<RefCell<i32>>valueListaConsvaluevalueavalue5valueaavalue

我们将列表包装在 so 中,当我们创建列表时,它们 都可以引用 ,这就是我们在示例 15-18 中所做的。aRc<T>bca

在 、 和 中创建列表后,我们想将 10 添加到 值。我们通过调用 来实现这一点,它使用 我们在第 5 章中讨论的自动取消引用功能(参见 “-> 运算符在哪里?”) 取消引用 the 到 inner 值。该方法返回一个智能指针,我们使用 dereference 运算符 并更改内部值。abcvalueborrow_mutvalueRc<T>RefCell<T>borrow_mutRefMut<T>

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

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

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

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