Rust智能指针RefCell<T>

内部可变性(Interior mutability)是Rust中的一个设计模式,它允许你在有不可变引用时也可以改变数据。这需要在数据结构中使用unsafe代码来模糊Rust的可变性与借用规则。

可以确保代码在运行时会遵守借用规则,可以使用运用内部可变性模式的类型,涉及unsafe代码被封装进安全的API中。外部类型依旧是不可变的。

使用RefCell在运行时检查借用规则🔗

ReCell<T>代表数据的唯一所有权,RefCell允许在运行时检查借用规则而不是编译时。在运行时违反规则程序会panic。

在一些特定的内存安全场景,虽没违反规则,但是在编译时检查却不被允许,RefCell<T>就派上用场了。

类似于Rc<T>,RefCell<T>也只能用于单线程场景,下面是选择Box<T>、Rc<T>还是RefCell<T>的理由:

  • Rc<T>允许相同数据有多个所有者,RefCell<T>和Box<T>有单一所有者。
  • Box<T>在编译时执行不可变或可变借用检查,Rc<T>在编译时执行不可变借用检查,RefCell<T>在运行时执行不可变或可变借用检查。
  • 允许在RefCell<T>自身不可变的情况下修改其内部的值。

内部可变性用例:mock对象🔗

有时程序员会用某个类型替代另一个类型,观察其某些行为是否被正确实现。这个占位符类型称为测试替身(test double)。mock对象是特定类型的测试替身。

下面是我们想要测试的场景:编写一个记录某个值与最大值差距的库,并根据差距来发送消息,下面是代码:

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: &T, max: usize) -> LimitTracker<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!");
        }
    }
}

这些代码有一个Massenger trait,它有一个send方法,该方法接受一个字符串切片作为参数。这个trait是mock对象所要实现的接口库。然后我们要测试LimitTracker的set_value方法。

下面是测试代码:

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

这里定义了一个MockMessenger结构体,它有一个sent_messages字段,用于存储发送的消息。然后我们实现了Messenger trait,它有一个send方法,该方法接受一个字符串切片作为参数。这个方法将消息添加到sent_messages字段中。这个将limit_tracker的value设置为80,然后我们断言sent_messages字段中有一个消息。

然而测试有问题,因为send方法获取了self的不可变引用,这是我们使用RefCell<T>来解决这个问题:

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

这里send_messages的字段类型是RefCell<Vec<String>>而不是Vec<String>。

对于send方法的实现,我们使用了borrow_mut方法来获取sent_messages字段的可变引用。这样我们就可以修改sent_messages字段的值了。

最后在assert_eq!宏中,我们使用了borrow方法来获取sent_messages字段的不可变引用。这样我们就可以断言sent_messages字段的值了。

RefCell在运行时记录借用🔗

创建不可变和可变引用时,对RefCell<T>来说是使用borrow和borrow_mut方法。这两个方法都会返回一个Ref<T>和RefMut<T>,它们分别代表不可变和可变引用。

RefCell<T>会记录当前有多少活动的Ref<T>和RefMut<T>,RefCell<T>允许有多个不可变借用或一个可变借用。

结合Rc和RefCell来拥有多个可变数据所有者🔗

Rc<T>允许对相同数据有多个所有者,不过只能提供不可变访问。一个储存了RefCell<T>的Rc<T>可以得到多个所有者并且可以修改的值。下面是链表的例子:

#[derive(Debug)]
enum List {
    Cons(Rc<RefCell<i32>>, Rc<List>),
    Nil,
}

use 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);
}

这里创建的一个Rc<RefCell<i32>>实例储存在变量value中,然后通过它创建了a列表,用到了克隆使a和value都拥有内部RefCell<i32>的所有权。

同样将a克隆到b和c中,然后我们使用borrow_mut方法来获取value的可变引用并将其值增加10(这里使用了自动解引用得到了RefCell<i32>,然后调用borrow_mut方法来获取i32的可变引用,最后解引用后修改其值)。最后打印出a、b和c的值,可以看到它们的值都被修改了。

使用RefCell<T>我们创建了一个表面上不可变但实际可修改数据的数据结构,同时其规则也保护我们免于数据竞争。

🔥 谨慎

RefCell<T>不能用于多线程代码,将在后面讨论Mutex<T>这个线程安全版本的RefCell<T>。