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();
}
在这个示例中,我们创建了一个有趣的设计模式:
Bar结构体包含一个RefCell<Foo>和一个StringFoo结构体有一个方法foo,它需要&mut self和&Bar作为参数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 实例可以共享同一个 RefCelllet 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_borrow 和 try_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();
}
这段代码展示了几个重要概念:
- 内部可变性:
Bar结构体通过RefCell<Foo>实现了内部可变性 - 方法调用链:
bar方法通过 RefCell 获取Foo的可变引用,然后调用Foo的方法 - 循环引用:
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 中实现内部可变性的重要工具,它允许我们在不可变的容器中修改数据:
- 通过运行时借用检查而不是编译时检查来工作
- 适用于未实现 Copy 语义的类型
- 与
Rc<T>结合可以实现多所有权的可变数据 - 在"对内可变,对外不可变"的设计模式中非常有用
关键要点:
borrow()获取不可变引用borrow_mut()获取可变引用- 违反借用规则会导致运行时 panic
- 有运行时开销,应谨慎使用
- 常与 Rc 配合使用以实现复杂的数据结构
通过合理使用 RefCell,我们可以编写出更加灵活的 Rust 代码,同时保持内存安全。
