键盘快捷键

按 ← 或 → 在章节之间导航

按 S 或 / 在本书中搜索

按 ? 显示此帮助

按 Esc 隐藏此帮助

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

内部可变性(interior mutability)是 Rust 中的一种设计模式,它允许你即使在存在指向该数据的不可变引用时也能修改数据;通常,这种操作是被借用规则禁止的。为了修改数据,该模式在数据结构内部使用 unsafe 代码来扭曲 Rust 通常管理可变和借用的规则。不安全代码向编译器表明我们正在手动检查规则,而不是依赖编译器替我们检查;我们将在第 20 章更详细地讨论 unsafe 代码。

只有当我们能够确保在运行时会遵守借用规则时,才能使用采用内部可变性模式的类型,即使编译器无法保证这一点。所涉及的 unsafe 代码随后被包装在一个安全的 API 中,而外部类型仍然是不可变的。

让我们通过研究遵循内部可变性模式的 RefCell<T> 类型来探索这个概念。

在运行时强制执行借用规则

与 Rc<T> 不同,RefCell<T> 类型代表对其持有的数据的单一所有权。那么,是什么让 RefCell<T> 与 Box<T> 这样的类型不同呢?回想一下你在第 4 章学到的借用规则:

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

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

在编译时检查借用规则的优点是错误会在开发过程中更早被发现,并且对运行时性能没有影响,因为所有的分析都是预先完成的。出于这些原因,在大多数情况下在编译时检查借用规则是最佳选择,这也是为什么这是 Rust 的默认行为。

相反,在运行时检查借用规则的优点是某些内存安全的场景被允许了,而它们原本会被编译时检查所禁止。像 Rust 编译器这样的静态分析本质上是保守的。代码的某些属性无法通过分析代码来检测:最著名的例子是停机问题(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> 内部的值。

修改一个不可变值内部的值就是内部可变性模式。让我们来看一个内部可变性很有用的场景,并研究它是如何实现的。

使用内部可变性

借用规则的一个后果是,当你有一个不可变的值时,你不能可变地借用它。例如,这段代码无法编译:

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

然而,在某些情况下,一个值在自己的方法中修改自己但对其他代码表现为不可变是很有用的。值的方法之外的代码将无法修改该值。使用 RefCell<T> 是获得内部可变性能力的一种方式,但 RefCell<T> 并不能完全绕过借用规则:编译器中的借用检查器允许这种内部可变性,而借用规则改为在运行时检查。如果你违反了规则,你会得到一个 panic! 而不是编译器错误。

让我们通过一个实际例子来了解如何使用 RefCell<T> 修改一个不可变的值,并看看为什么这很有用。

使用 Mock 对象进行测试

有时在测试过程中,程序员会使用一种类型来代替另一种类型,以便观察特定行为并断言其实现正确。这种占位符类型称为测试替身(test double)。可以把它想象成电影制作中的特技替身,即一个人代替演员来完成某个特别棘手的场景。在运行测试时,测试替身代替其他类型。Mock 对象是测试替身的特定类型,它们记录测试期间发生的事情,以便你可以断言正确的操作已经发生。

Rust 不像其他语言那样有对象的概念,而且 Rust 的标准库也不像某些其他语言那样内置了 mock 对象功能。但是,你完全可以创建一个结构体来充当 mock 对象的角色。

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

我们的库只提供跟踪值接近最大值的程度以及在什么时间应该发送什么消息的功能。使用我们库的应用程序需要提供发送消息的机制:应用程序可以直接向用户显示消息、发送电子邮件、发送短信或做其他事情。库不需要知道这些细节。它只需要某个实现了我们将提供的一个叫做 Messenger 的 trait 的东西。Listing 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!");
        }
    }
}
Listing 15-20: 一个跟踪值接近最大值程度并在值达到特定级别时发出警告的库

这段代码的一个重要部分是 Messenger trait 有一个叫做 send 的方法,它接受对 self 的不可变引用和消息文本。这个 trait 是我们的 mock 对象需要实现的接口,这样 mock 就能像真实对象一样被使用。另一个重要部分是我们想测试 LimitTracker 上 set_value 方法的行为。我们可以改变传入 value 参数的值,但 set_value 不返回任何东西让我们做断言。我们希望能够说,如果我们用一个实现了 Messenger trait 的东西和一个特定的 max 值创建了一个 LimitTracker,那么当我们为 value 传入不同的数字时,messenger 会被告知发送适当的消息。

我们需要一个 mock 对象,它在我们调用 send 时不发送电子邮件或短信,而只是记录它被告知要发送的消息。我们可以创建一个 mock 对象的新实例,创建一个使用该 mock 对象的 LimitTracker,在 LimitTracker 上调用 set_value 方法,然后检查 mock 对象是否有我们期望的消息。Listing 15-21 展示了一个尝试实现这样一个 mock 对象的代码,但借用检查器不允许。

文件名: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);
    }
}
Listing 15-21: 一个实现 MockMessenger 的尝试,但不被借用检查器允许

这段测试代码定义了一个 MockMessenger 结构体,它有一个 sent_messages 字段,类型是 String 值的 Vec,用来记录它被告知要发送的消息。我们还定义了一个关联函数 new,以便方便地创建以空消息列表开头的新 MockMessenger 值。然后,我们为 MockMessenger 实现了 Messenger trait,这样我们就可以把一个 MockMessenger 交给 LimitTracker。在 send 方法的定义中,我们把作为参数传入的消息取出,并将其存储在 MockMessenger 的 sent_messages 列表中。

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

然而,这个测试有一个问题,如下所示:

$ 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 it 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 来记录消息,因为 send 方法接受对 self 的不可变引用。我们也不能采纳错误文本中的建议,在 impl 方法和 trait 定义中都使用 &mut self。我们不想仅仅为了测试而改变 Messenger trait。相反,我们需要找到一种方法,让我们的测试代码在现有设计下正确工作。

这正是内部可变性可以帮助的情况!我们将把 sent_messages 存储在一个 RefCell<T> 中,这样 send 方法就能修改 sent_messages 来存储我们看到的消息。Listing 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);
    }
}
Listing 15-22: 使用 RefCell<T> 在外部值被认为不可变的情况下修改内部值

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

对于 send 方法的实现,第一个参数仍然是对 self 的不可变借用,这与 trait 定义一致。我们对 self.sent_messages 中的 RefCell<Vec<String>> 调用 borrow_mut,以获取对 RefCell<Vec<String>> 内部值(即向量)的可变引用。然后,我们可以在向量的可变引用上调用 push,以记录测试期间发送的消息。

我们必须做的最后一处改动是在断言中:为了查看内部向量中有多少项,我们对 RefCell<Vec<String>> 调用 borrow 以获取对向量的不可变引用。

既然你已经看到了如何使用 RefCell<T>,让我们深入了解它的工作原理!

在运行时跟踪借用

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

RefCell<T> 会跟踪当前有多少个 Ref<T> 和 RefMut<T> 智能指针处于活动状态。每次我们调用 borrow,RefCell<T> 都会增加其活动不可变借用的计数。当一个 Ref<T> 值离开作用域时,不可变借用的计数减少 1。就像编译时借用规则一样,RefCell<T> 允许我们在任意时间点拥有多个不可变借用或一个可变借用。

如果我们试图违反这些规则,与引用不同的是我们不会得到编译器错误,RefCell<T> 的实现会在运行时 panic。Listing 15-23 展示了对 Listing 15-22 中 send 实现的修改。我们故意尝试在同一作用域内创建两个活动的可变借用,以说明 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);
    }
}
Listing 15-23: 在同一作用域内创建两个可变引用以查看 RefCell<T> 是否会 panic

我们为 borrow_mut 返回的 RefMut<T> 智能指针创建了一个变量 one_borrow。然后,我们以同样的方式在变量 two_borrow 中创建了另一个可变借用。这就在同一作用域内产生了两个可变引用,这是不允许的。当我们运行库的测试时,Listing 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' (6028024) panicked at src/lib.rs:60:53:
RefCell already borrowed
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`

注意,代码以 already borrowed: BorrowMutError 消息 panic 了。这就是 RefCell<T> 在运行时处理违反借用规则的方式。

像我们在这里所做的那样,选择在运行时而不是编译时捕获借用错误,意味着你可能会在开发过程的后期才发现代码中的错误:可能要等到你的代码部署到生产环境。此外,由于在运行时而不是编译时跟踪借用,你的代码会产生一点运行时性能损失。然而,使用 RefCell<T> 使得编写一个 mock 对象成为可能,它可以在只允许不可变值的上下文中使用时修改自身来记录它看到的消息。尽管 RefCell<T> 有权衡,但你可以使用它来获得比常规引用更多的功能。

允许多个所有者拥有可变数据

使用 RefCell<T> 的一种常见方式是与 Rc<T> 结合使用。回想一下,Rc<T> 让你可以拥有某些数据的多个所有者,但它只提供对该数据的不可变访问。如果你有一个持有 RefCell<T> 的 Rc<T>,你就可以得到一个既能有多个所有者又能修改的值!

例如,回想一下 Listing 15-18 中的 cons list 示例,我们使用 Rc<T> 允许多个列表共享对另一个列表的所有权。因为 Rc<T> 只持有不可变值,一旦创建了列表,我们就无法改变其中的任何值。让我们加入 RefCell<T>,利用它能够改变列表中值的能力。Listing 15-24 展示了通过在 Cons 定义中使用 RefCell<T>,我们可以修改存储在所有列表中的值。

文件名: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:?}");
}
Listing 15-24: 使用 Rc<RefCell<i32>> 创建一个我们可以修改的 List

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

我们将列表 a 包装在 Rc<T> 中,这样当我们创建列表 b 和 c 时,它们都可以引用 a,这与我们在 Listing 15-18 中所做的一样。

在 a、b 和 c 中创建列表之后,我们想给 value 中的值加 10。我们通过对 value 调用 borrow_mut 来做到这一点,它使用了我们在第 5 章「-> 运算符在哪里?」中讨论的自动解引用功能,将 Rc<T> 解引用为内部的 RefCell<T> 值。borrow_mut 方法返回一个 RefMut<T> 智能指针,我们对它使用解引用运算符并改变内部值。

当我们打印 a、b 和 c 时,我们可以看到它们的值都变成了 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> 的线程安全版本,我们将在第 16 章讨论 Mutex<T>。