在之前的练习中,我们学习了 RefCell 的基本概念和用法。今天我们将通过一个具体的示例来深入了解 RefCell 在实际场景中的应用,特别是"对内可变,对外不可变"这一重要设计模式。

RefCell 的核心理念

RefCell 实现了 Rust 中的"内部可变性"模式。这种模式允许我们在不可变的容器中修改数据,这在某些场景下非常有用,尤其是在需要绕过 Rust 严格的借用检查规则时。

正如 README 中提到的:

对内可变,对外不可变

这意味着我们可以保持结构体的不可变性,同时允许其内部某些字段具有可变性。

项目中的示例代码

让我们先看看项目中的示例代码:

use std::cell::RefCell;
use std::rc::Rc;
use std::sync::{mpsc, Arc};
use std::thread;

struct Foo;

struct Bar {
    foo: RefCell<Foo>,
    msg: String,
}

impl Foo {
    fn foo(&mut self, bar: &Bar) {
        println!("{}", bar.msg);
    }
}

impl Bar {
    fn new(foo: Foo) -> Self {
        Self {
            foo: RefCell::new(foo),
            msg: String::from("Baaaaaar"),
        }
    }
    fn bar(&mut self) {
        self.foo.borrow_mut().foo(self);
    }
}

fn main() {
    let mut bar = Bar::new(Foo);
    bar.bar();
}

在这个示例中,我们创建了一个有趣的设计模式:

  1. Bar 结构体包含一个 RefCell<Foo> 和一个 String
  2. Foo 结构体有一个方法 foo,它需要 &mut self&Bar 作为参数
  3. Bar 结构体有一个方法 bar,它通过 RefCell 获取 Foo 的可变引用并调用其方法

这种设计允许我们在 Bar 的不可变方法中修改 Foo,同时保持 Bar 本身的不可变性。

RefCell 工作原理

正如 README 中解释的:

RefCell会记录当前存在多少个活跃的 Ref和 RefMut 智能指针:

  • 调用 borrow 时,不可变借用计数加1
  • 任意 Ref的值离开作用域时(释放),不可变借用计数减1
  • 每次调用 borrow_mut: 可变借用计数加1
  • 任何 RefMut 离开使用域时(释放),可变借用计数减1

实际应用场景

1. 图数据结构

use std::cell::RefCell;
use std::rc::Rc;

#[derive(Debug)]
struct Node {
    value: i32,
    children: RefCell<Vec<Rc<Node>>>,
    parent: RefCell<Option<Rc<Node>>>,
}

impl Node {
    fn new(value: i32) -> Rc<Node> {
        Rc::new(Node {
            value,
            children: RefCell::new(vec![]),
            parent: RefCell::new(None),
        })
    }
    
    fn add_child(self: Rc<Node>, child: Rc<Node>) {
        *child.parent.borrow_mut() = Some(self.clone());
        self.children.borrow_mut().push(child);
    }
}

fn graph_example() {
    let root = Node::new(1);
    let child1 = Node::new(2);
    let child2 = Node::new(3);
    
    root.clone().add_child(child1);
    root.clone().add_child(child2);
    
    println!("Root: {:?}", root);
}

2. 观察者模式

use std::cell::RefCell;
use std::rc::Rc;

trait Observer {
    fn notify(&self, message: &str);
}

struct Subject {
    observers: RefCell<Vec<Rc<dyn Observer>>>,
    state: String,
}

impl Subject {
    fn new() -> Subject {
        Subject {
            observers: RefCell::new(vec![]),
            state: String::new(),
        }
    }
    
    fn attach(&self, observer: Rc<dyn Observer>) {
        self.observers.borrow_mut().push(observer);
    }
    
    fn set_state(&self, state: String) {
        self.state = state;
        self.notify_observers();
    }
    
    fn notify_observers(&self) {
        for observer in self.observers.borrow().iter() {
            observer.notify(&self.state);
        }
    }
}

struct ConcreteObserver {
    name: String,
}

impl ConcreteObserver {
    fn new(name: String) -> ConcreteObserver {
        ConcreteObserver { name }
    }
}

impl Observer for ConcreteObserver {
    fn notify(&self, message: &str) {
        println!("Observer {}: Received message '{}'", self.name, message);
    }
}

fn observer_pattern_example() {
    let subject = Rc::new(Subject::new());
    let observer1 = Rc::new(ConcreteObserver::new("A".to_string()));
    let observer2 = Rc::new(ConcreteObserver::new("B".to_string()));
    
    subject.attach(observer1);
    subject.attach(observer2);
    
    subject.set_state("New State".to_string());
}

3. Mock 对象和测试

use std::cell::RefCell;

#[derive(Debug)]
struct MockNetworkClient {
    calls: RefCell<Vec<String>>,
    response: String,
}

impl MockNetworkClient {
    fn new(response: String) -> MockNetworkClient {
        MockNetworkClient {
            calls: RefCell::new(vec![]),
            response,
        }
    }
    
    fn get_calls(&self) -> Vec<String> {
        self.calls.borrow().clone()
    }
    
    fn send_request(&self, url: String) -> String {
        self.calls.borrow_mut().push(url);
        self.response.clone()
    }
}

fn mock_example() {
    let mock_client = MockNetworkClient::new("Mock Response".to_string());
    
    let response1 = mock_client.send_request("http://example.com/1".to_string());
    let response2 = mock_client.send_request("http://example.com/2".to_string());
    
    assert_eq!(response1, "Mock Response");
    assert_eq!(response2, "Mock Response");
    
    let calls = mock_client.get_calls();
    assert_eq!(calls.len(), 2);
    assert_eq!(calls[0], "http://example.com/1");
    assert_eq!(calls[1], "http://example.com/2");
}

与 Rc 配合使用

RefCell 经常与 Rc(引用计数)配合使用,以实现多所有权的可变数据:

use std::cell::RefCell;
use std::rc::Rc;

fn rc_refcell_example() {
    let data = Rc::new(RefCell::new(5));
    
    // 多个 Rc 实例可以共享同一个 RefCell
    let data1 = data.clone();
    let data2 = data.clone();
    
    // 通过任何一个 Rc 实例都可以修改数据
    *data1.borrow_mut() += 1;
    *data2.borrow_mut() += 1;
    
    println!("Value: {}", *data.borrow()); // 输出: 7
}

与 Arc 配合使用(线程安全版本)

对于多线程环境,我们可以使用 Arc 和 Mutex:

use std::sync::{Arc, Mutex};
use std::thread;

fn arc_mutex_example() {
    let data = Arc::new(Mutex::new(5));
    
    let data1 = data.clone();
    let data2 = data.clone();
    
    let handle1 = thread::spawn(move || {
        let mut num = data1.lock().unwrap();
        *num += 1;
    });
    
    let handle2 = thread::spawn(move || {
        let mut num = data2.lock().unwrap();
        *num += 1;
    });
    
    handle1.join().unwrap();
    handle2.join().unwrap();
    
    println!("Value: {}", *data.lock().unwrap()); // 输出: 7
}

错误处理

RefCell 在运行时检查借用规则,如果违反规则会导致 panic。我们可以使用 try_borrowtry_borrow_mut 来避免 panic:

use std::cell::RefCell;

fn error_handling_example() {
    let data = RefCell::new(5);
    
    // 获取不可变引用
    let _borrowed = data.borrow();
    
    // 尝试获取可变引用(不会 panic)
    match data.try_borrow_mut() {
        Ok(_mut_borrowed) => {
            println!("Got mutable borrow");
        }
        Err(_) => {
            println!("Could not get mutable borrow - already borrowed");
        }
    }
}

与项目代码的深入分析

让我们回到原始项目代码,深入分析其中的设计模式:

use std::cell::RefCell;
use std::rc::Rc;
use std::sync::{mpsc, Arc};
use std::thread;

struct Foo;

struct Bar {
    foo: RefCell<Foo>,
    msg: String,
}

impl Foo {
    fn foo(&mut self, bar: &Bar) {
        println!("{}", bar.msg);
    }
}

impl Bar {
    fn new(foo: Foo) -> Self {
        Self {
            foo: RefCell::new(foo),
            msg: String::from("Baaaaaar"),
        }
    }
    fn bar(&mut self) {
        self.foo.borrow_mut().foo(self);
    }
}

fn main() {
    let mut bar = Bar::new(Foo);
    bar.bar();
}

这段代码展示了几个重要概念:

  1. 内部可变性Bar 结构体通过 RefCell<Foo> 实现了内部可变性
  2. 方法调用链bar 方法通过 RefCell 获取 Foo 的可变引用,然后调用 Foo 的方法
  3. 循环引用Foo::foo 方法需要 &Bar 参数,而 Bar::bar 方法调用 Foo::foo 并传递 self

这种设计模式在以下场景中非常有用:

  • 当我们需要在不可变的结构体中修改某些内部状态时
  • 当我们需要打破 Rust 的借用规则以实现特定的设计模式时
  • 当我们需要在方法调用中传递包含正在修改对象的引用时

最佳实践

1. 合理使用 RefCell

use std::cell::RefCell;

// 好的做法:在需要内部可变性时使用 RefCell
struct Counter {
    value: RefCell<i32>,
}

impl Counter {
    fn new(initial: i32) -> Counter {
        Counter {
            value: RefCell::new(initial),
        }
    }
    
    fn increment(&self) {
        *self.value.borrow_mut() += 1;
    }
    
    fn get_value(&self) -> i32 {
        *self.value.borrow()
    }
}

// 避免:在不需要内部可变性时使用 RefCell
fn avoid_unnecessary_refcell() {
    // 如果不需要内部可变性,直接使用变量即可
    // let data = RefCell::new(5);
    // let value = *data.borrow();
    
    let data = 5; // 更简单直接
    println!("Value: {}", data);
}

2. 注意运行时开销

use std::cell::RefCell;

fn runtime_overhead_awareness() {
    let data = RefCell::new(42);
    
    // 每次 borrow/borrow_mut 都有运行时检查开销
    for _ in 0..1000 {
        *data.borrow_mut() += 1;
    }
    
    println!("Final value: {}", *data.borrow());
}

总结

RefCell 是 Rust 中实现内部可变性的重要工具,它允许我们在不可变的容器中修改数据:

  1. 通过运行时借用检查而不是编译时检查来工作
  2. 适用于未实现 Copy 语义的类型
  3. Rc<T> 结合可以实现多所有权的可变数据
  4. 在"对内可变,对外不可变"的设计模式中非常有用

关键要点:

  • borrow() 获取不可变引用
  • borrow_mut() 获取可变引用
  • 违反借用规则会导致运行时 panic
  • 有运行时开销,应谨慎使用
  • 常与 Rc 配合使用以实现复杂的数据结构

通过合理使用 RefCell,我们可以编写出更加灵活的 Rust 代码,同时保持内存安全。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐