Skip to content

Day 22: 智能指针:Rc与RefCell

在昨天的学习中,我们了解了Box——一种用于堆分配和动态分发的智能指针。但在实际开发中,我们经常需要共享所有权(shared ownership)的场景:一个数据被多个所有者共同拥有。Rust的所有权模型是严格的,每个值只能有一个所有者。Rc(reference counting)解决了这个问题。

今天我们还将学习RefCell,它提供了内部可变性(interior mutability),让我们能够在不可变引用的前提下修改数据。

Rc允许多个所有者共享同一份数据,通过引用计数追踪所有者的数量。当引用计数归零时,数据才会被释放。

use std::rc::Rc;
fn main() {
let data = Rc::new(5);
println!("初始引用计数: {}", Rc::strong_count(&data));
// 克隆会增加引用计数(不会深拷贝数据)
let data2 = Rc::clone(&data);
println!("克隆后引用计数: {}", Rc::strong_count(&data));
println!("data2的值: {}", data2);
// data和data2共享同一份数据
{
let data3 = Rc::clone(&data);
println!("作用域内引用计数: {}", Rc::strong_count(&data));
} // data3离开作用域,计数减1
println!("作用域外引用计数: {}", Rc::strong_count(&data));
} // data和data2离开作用域,计数归零,数据被释放

Rc::clone()的时间复杂度是O(1),比真正的深拷贝要快得多,因为它只增加了引用计数。

Rc最适合用于”读多写少”的共享场景,例如:

use std::rc::Rc;
#[derive(Debug)]
struct Node {
value: i32,
children: Vec<Rc<Node>>,
}
fn main() {
// 创建叶子节点
let leaf = Rc::new(Node {
value: 3,
children: Vec::new(),
});
// 创建分支节点,共享leaf的引用
let branch = Rc::new(Node {
value: 5,
children: vec![Rc::clone(&leaf)],
});
println!("leaf引用计数: {}", Rc::strong_count(&leaf));
println!("branch引用计数: {}", Rc::strong_count(&branch));
}

注意:Rc是非线程安全的。如果需要在多线程间共享数据,应该使用Arc(atomic reference counting),我们明天会学习。

RefCell是另一种重要的智能指针,它提供了内部可变性。正常情况下,Rust的借用规则不允许通过不可变引用修改数据,但RefCell打破了这个限制。

use std::cell::RefCell;
fn main() {
let data = RefCell::new(5);
// 通过borrow()获取不可变引用
println!("值: {}", data.borrow());
// 通过borrow_mut()获取可变引用
*data.borrow_mut() = 10;
println!("修改后的值: {}", data.borrow());
}

RefCell将借用检查从编译时移到运行时。如果你在持有不可变引用时试图获取可变引用,程序会panic而不是编译错误。

Rc和RefCell的组合是Rust中非常常见的模式:Rc提供共享所有权,RefCell提供内部可变性。

use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug)]
struct Config {
timeout: RefCell<u64>,
}
fn main() {
let config = Rc::new(Config {
timeout: RefCell::new(30),
});
// 多个地方可以共享config
fn update_timeout(cfg: &Rc<Config>, new_timeout: u64) {
*cfg.timeout.borrow_mut() = new_timeout;
}
fn read_timeout(cfg: &Rc<Config>) -> u64 {
*cfg.timeout.borrow()
}
println!("原始超时: {}", read_timeout(&config));
update_timeout(&config, 60);
println!("更新后超时: {}", read_timeout(&config));
}

这种模式让我们可以在多个所有者之间共享可变状态,而不需要将所有权转移给任何一个人。

使用Rc和RefCell时需要小心循环引用。如果两个Rc互相引用,引用计数永远无法归零,造成内存泄漏。

use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug)]
struct Node {
value: i32,
// 错误示例:不要这样做
next: RefCell<Option<Rc<Node>>>,
}
fn main() {
let node1 = Rc::new(Node {
value: 1,
next: RefCell::new(None),
});
let node2 = Rc::new(Node {
value: 2,
next: RefCell::new(None),
});
// 创建循环引用
*node1.next.borrow_mut() = Some(Rc::clone(&node2));
*node2.next.borrow_mut() = Some(Rc::clone(&node1));
println!("node1计数: {}", Rc::strong_count(&node1));
println!("node2计数: {}", Rc::strong_count(&node2));
// 即使离开作用域,计数也不会归零,造成内存泄漏
}

方法一:使用Weak

Weak是Rc的”弱引用”,它不会增加引用计数,需要手动upgrade()转换为Rc。

use std::cell::RefCell;
use std::rc::{Rc, Weak};
#[derive(Debug)]
struct TreeNode {
value: i32,
parent: RefCell<Weak<TreeNode>>,
children: RefCell<Vec<Rc<TreeNode>>>,
}
fn main() {
let leaf = Rc::new(TreeNode {
value: 3,
parent: RefCell::new(Weak::new()),
children: RefCell::new(Vec::new()),
});
let branch = Rc::new(TreeNode {
value: 5,
parent: RefCell::new(Weak::new()),
children: RefCell::new(Vec::new()),
});
// leaf是branch的子节点
*branch.children.borrow_mut() = vec![Rc::clone(&leaf)];
// branch是leaf的父节点(使用Weak,不增加计数)
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);
println!("leaf强引用: {}", Rc::strong_count(&leaf));
println!("leaf弱引用: {}", Rc::weak_count(&leaf));
}

方法二:重新设计数据结构

有时候更好的方法是避免双向引用。例如,在树结构中,只让父节点拥有子节点,子节点通过其他方式(如遍历)找到父节点。

今天我们学习了两种重要的智能指针:

  1. Rc:提供共享所有权,通过引用计数管理数据生命周期。适用于同一数据的多个所有者场景。

  2. RefCell:提供内部可变性,在运行时检查借用规则。适用于需要在不可变引用存在时修改数据的场景。

Rc和RefCell组合是Rust中处理共享可变状态的标准模式。但要特别注意避免循环引用,必要时使用Weak打破循环。