Day 22: 智能指针:Rc与RefCell
在昨天的学习中,我们了解了Box——一种用于堆分配和动态分发的智能指针。但在实际开发中,我们经常需要共享所有权(shared ownership)的场景:一个数据被多个所有者共同拥有。Rust的所有权模型是严格的,每个值只能有一个所有者。Rc(reference counting)解决了这个问题。
今天我们还将学习RefCell,它提供了内部可变性(interior mutability),让我们能够在不可变引用的前提下修改数据。
2. Rc 引用计数
Section titled “2. Rc 引用计数”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),比真正的深拷贝要快得多,因为它只增加了引用计数。
3. Rc 的实际应用场景
Section titled “3. Rc 的实际应用场景”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),我们明天会学习。
4. RefCell 内部可变性
Section titled “4. RefCell 内部可变性”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而不是编译错误。
5. Rc 和 RefCell 组合模式
Section titled “5. Rc 和 RefCell 组合模式”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));}这种模式让我们可以在多个所有者之间共享可变状态,而不需要将所有权转移给任何一个人。
6. 循环引用与内存泄漏
Section titled “6. 循环引用与内存泄漏”使用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)); // 即使离开作用域,计数也不会归零,造成内存泄漏}7. 规避循环引用的方法
Section titled “7. 规避循环引用的方法”方法一:使用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));}方法二:重新设计数据结构
有时候更好的方法是避免双向引用。例如,在树结构中,只让父节点拥有子节点,子节点通过其他方式(如遍历)找到父节点。
今天我们学习了两种重要的智能指针:
-
Rc:提供共享所有权,通过引用计数管理数据生命周期。适用于同一数据的多个所有者场景。
-
RefCell:提供内部可变性,在运行时检查借用规则。适用于需要在不可变引用存在时修改数据的场景。
Rc和RefCell组合是Rust中处理共享可变状态的标准模式。但要特别注意避免循环引用,必要时使用Weak打破循环。