Rust 练习册 14:RefCell 实际使用场景 - 教程

在之前的练习中,我们学习了 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 代码,同时保持内存安全。

posted @ 2025-12-05 08:04  gccbuaa  阅读(0)  评论(0)    收藏  举报