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 实例可以共享同一个 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_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 代码,同时保持内存安全。

浙公网安备 33010602011771号