Rust:通过几个例子学习 Cell 和 RefCell

简介

在 Rust 编程语言中,Cell 和 RefCell 是两个非常重要的工具。尽管它们的名字听起来像是简单的包装器,但它们实际上解决了一些复杂的现实问题。本文将深入探讨 Cell 和 RefCell,并通过具体的实例说明它们在 Rust 编程中的重要性。

Cell 和 RefCell

首先,Cell 和 RefCell 并不是指针类型,它们是围绕其他类型的包装器,提供了一种在不可变引用下修改数据的方法。

Cell 和 RefCell 的核心在于它们是对不安全类型 UnsafeCell 的安全抽象。UnsafeCell 是一个特殊的类型,编译器知道如何特殊处理通过它的访问。然而,UnsafeCell 本身并不安全。因此,Cell 和 RefCell 提供了对 UnsafeCell 的安全封装,使我们可以在保证安全的前提下使用它的功能。

  • 什么是安全? 在 Rust 中,安全是指避免内存安全问题,例如数据竞争、空指针解引用、悬空指针等。Rust 的所有权和借用检查机制在编译时确保代码安全。然而,某些情况下,我们需要在不可变引用下修改数据,这就需要 Cell 或 RefCell 的帮助。

Cell 的使用场景

Cell 要求被包装的类型必须实现 Copy trait。这意味着 Cell 可以自由地复制和修改值而无需考虑析构。Cell 的使用相对简单,因为它不需要复杂的操作。

一个典型的使用 Cell 的场景是存储简单的数值类型。例如,我们可以使用 Cell 来存储一个计数器:

 1use std::cell::Cell;
 2
 3struct Counter {
 4    value: Cell<i32>,
 5}
 6
 7impl Counter {
 8    fn new() -> Counter {
 9        Counter { value: Cell::new(0) }
10    }
11
12    fn increment(&self) {
13        let current = self.value.get();
14        self.value.set(current + 1);
15    }
16
17    fn get(&self) -> i32 {
18        self.value.get()
19    }
20}
21
22fn main() {
23    let counter = Counter::new();
24    counter.increment();
25    println!("Counter: {}", counter.get());
26}

在这个例子中,Counter 使用 Cell 来存储一个可在不可变引用下修改的计数器。

为什么需要在不可变引用下修改计数器?

在 Rust 中,一个变量可以有多个不可变引用(&T)或一个可变引用(&mut T),但不能同时存在。因此,如果计数器需要在多个地方共享,并且每个地方都可能会修改计数器的值,直接使用可变引用(&mut)是不行的。这时,Cell 或 RefCell 就派上用场了。

由于 Cell 需要实现 Copy trait,因此它有局限性,比如不能在 Cell 中存储 String 或 Vec。

RefCell 的使用场景

RefCell 更加灵活,它允许你存储几乎任何类型,包括上面提到的实现 Copy trait 的类型,但需要在运行时进行动态借用检查。Rust 的借用规则要求我们在共享和可变之间做出选择,而 RefCell 在运行时通过跟踪借用的次数和方式来强制执行这一点。如果检测到借用规则被违反,它会触发 panic。

RefCell 的一个典型使用场景是实现缓存或记忆化(memoization)。假设我们有一个函数,它的计算结果需要缓存以提高性能:

 1use std::cell::RefCell;
 2use std::collections::HashMap;
 3
 4struct Memoizer {
 5    cache: RefCell<HashMap<i32, i32>>,
 6}
 7
 8impl Memoizer {
 9    fn new() -> Memoizer {
10        Memoizer {
11            cache: RefCell::new(HashMap::new()),
12        }
13    }
14
15    fn compute(&self, arg: i32) -> i32 {
16        if let Some(&result) = self.cache.borrow().get(&arg) {
17            return result;
18        }
19
20        // 假设这是一个昂贵的计算
21        let result = arg * arg;
22        self.cache.borrow_mut().insert(arg, result);
23        result
24    }
25}
26
27fn main() {
28    let memoizer = Memoizer::new();
29    println!("Result: {}", memoizer.compute(10));
30    println!("Result: {}", memoizer.compute(10)); // 这次将从缓存中获取结果
31}

在这个例子中,Memoizer 使用 RefCell 来存储一个可变的 HashMap,即使 Memoizer 本身是不可变的。这使得我们可以在不暴露内部实现细节的情况下实现缓存。对于使用者而言,Memoizer 看起来是一个不可变的缓存引用,但实际上其内部利用可变性来工作,从而简化了使用者的操作。

另一个例子:线程本地存储

thread_local! 宏用于定义线程本地存储(TLS),即每个线程都有自己独立的变量副本。对于每个线程来说,这些变量是独立的,互不干扰。以下是一个示例:

 1use std::cell::Cell;
 2
 3thread_local! {
 4    static THREAD_LOCAL_DATA: Cell<u32> = Cell::new(0);
 5}
 6
 7fn main() {
 8    THREAD_LOCAL_DATA.with(|data| {
 9        data.set(42);
10        println!("Thread local data: {}", data.get());
11    });
12}

如果不使用 Cell,我们将无法在不可变引用的上下文中修改 u32 的值。因为 thread_local! 宏提供的访问方法(如 with)只允许通过不可变引用访问线程本地变量。

参考资料

What are Cell and RefCell used for? : r/rust