循环引用
对于前端同学来说,了解JS垃圾回收机制的话,一定也听过循环引用的概念,两个数据相互引用,想成一个环,由于环中每一个指针的引用计数都不可能减少到0,所以对应的值也不会被释放丢弃,这就造成了内存泄漏,前面我们学到的Rc<T>类型,同样存在这种问题。
制造循环引用
这里仍然使用前面的例子来试图制造两个相互引用的链表:
use std::rc::Rc;
use std::cell::RefCell;
#[derive(Debug)]
enum List {
Cons(i32, RefCell<Rc<List>>),
Nil
}
impl List {
// tail方法用来方便地访问Cons成员的第二项
fn tail(&self) -> Option<&RefCell<Rc<List>>> {
match self {
List::Cons(_, item) => Some(item),
List::Nil => None
}
}
}
// 这里在变量a中创建了一个Rc<List>实例来存放初值为5和Nil的List值
let a = Rc::new(List::Cons(5,
RefCell::new(
Rc::new(List::Nil)
)
));
println!("a 初始化后的引用数量 = {}", Rc::strong_count(&a));
// a 初始化后的引用数量 = 1
println!("a 的第二项是 = {:?}", a.tail());
// a 的第二项是 = Some(RefCell { value: Nil })
下面在变量b中创建了一个Rc<List>实例来存放初值为10和指向列表a的Rc<List>:
let b = Rc::new(List::Cons(10,
RefCell::new(
Rc::clone(&a)
)
));
println!("a 在 b 创建后的引用数量 = {}", Rc::strong_count(&a));
// a 在 b 创建后的引用数量 = 2
println!("b 初始化后的引用数量 = {}", Rc::strong_count(&b));
// b 初始化后的引用数量 = 1
println!("b 的第二项是 = {:?}", b.tail());
// b 的第二项是 = Some(RefCell { value: Cons(5, RefCell { value: Nil }) })
最后,把a的第二项指向b,造成循环引用:
if let Some(second) = a.tail() {
*second.borrow_mut() = Rc::clone(&b);
}
println!("改变a之后,b的引用数量 = {}", Rc::strong_count(&b));
// 改变a之后,b的引用数量 = 2
println!("改变a之后,a的引用数量 = {}", Rc::strong_count(&a));
// 改变a之后,a的引用数量 = 2
println!("a next item = {:?}", a);
// 报错,由于a和b相互引用,所以在打印过程中会无限打印,最终堆栈溢出
可以看到将 a 修改为指向 b 之后,a 和 b 中都有的 Rc<List> 实例的引用计数为 2。 在 main 的结尾,rust 会尝试首先丢弃 b,这会使 a 和 b 中 Rc<List> 实例的引用计数减 1。 然而,因为 a 仍然引用 b 中的 Rc<List>,Rc<List> 的引用计数是 1 而不是 0,由于其内存的引用计数为 1,所以 Rc<List> 在堆上的内存不会被丢弃,将会永久保留。
避免引用循环:将 Rc<T> 变为 Weak<T>
我们可以使用弱引用类型Weak<T>来防止循环引用:
// 引入Weak
use std::rc::{ Rc, Weak };
use std::cell::RefCell;
// 创建树形数据结构:带有子节点的 Node
#[derive(Debug)]
struct Node {
value: i32,
parent: RefCell<Weak<Node>>, // 对父节点的引用是弱引用
children: RefCell<Vec<Rc<Node>>> // 对子节点的引用是强引用
}
// 创建叶子结点
let leaf = Rc::new(Node {
value: 3,
children: RefCell::new(vec![]),
parent: RefCell::new(Weak::new())
});
// 创建枝干节点
let branch = Rc::new(Node {
value: 5,
// 将leaf作为branch的子节点
children: RefCell::new(vec![Rc::clone(&leaf)]),
parent: RefCell::new(Weak::new())
});
使用弱引用连接枝干和叶子节点:
// 与 Rc::clone 方法类似,
// 使用 Rc::downgrade 方法将leaf节点的父节点使用弱引用指向branch
*(leaf.parent.borrow_mut()) = Rc::downgrade(&branch);
// 使用upgrade方法查看父节点是否存在,返回Option类型,
// 可以成功打印,说明使用弱引用并没有造成循环引用
println!("leaf的parent节点 = {:?}", leaf.parent.borrow().upgrade());
// leaf的parent节点 = Some(Node {
// value: 5,
// parent: RefCell { value: (Weak) },
// children: RefCell {
// value: [
// Node {
// value: 3,
// parent: RefCell { val (Weak) },
// children: RefCell { value: [] }
// }
// ]
// }
// })
使用 Rc::downgrade 时会得到 Weak<T> 类型的智能指针,每次调用Rc::downgrade 会将 weak_count 加1,用于记录有多少个弱引用,而实例被清理时,关注的是strong_count,只要变成0就会清理,而不关心弱引用 weak_count 的数量。
观察 strong_count 和 weak_count 的改变
下面我们使用Rc::strong_count() 和 Rc::weak_count() 方法来观察一下强引用和弱引用的区别,注意他们在作用域销毁时的表现:
#[derive(Debug)]
struct Node {
value: i32,
parent: RefCell<Weak<Node>>,
children: RefCell<Vec<Rc<Node>>>,
}
let leaf = Rc::new(Node {
value: 3,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});
println!("子节点 强引用 = {}, 弱引用 = {}", Rc::strong_count(&leaf), Rc::weak_count(&leaf));
// 子节点 强引用 = 1, 弱引用 = 0
// 新作用域
{
let branch = Rc::new(Node {
value: 5,
parent: RefCell::new(Weak::new()),
// leaf放入branch子节点
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
// leaf父节点弱引用branch节点
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);
println!("branch 强引用 = {}, 弱引用 = {}", Rc::strong_count(&branch), Rc::weak_count(&branch));
// branch 强引用 = 1, 弱引用 = 1
println!("leaf 强引用 = {}, 弱引用 = {}", Rc::strong_count(&leaf), Rc::weak_count(&leaf));
// leaf 强引用 = 2, 弱引用 = 0
}
println!("leaf 的父节点 = {:?}", leaf.parent.borrow().upgrade());
// leaf 的父节点 = None,上面作用域销毁时,branch强引用从1
// 变成0,注意并不关注弱引用,即使弱引用为1,branch仍将被销毁
println!("leaf 强引用 = {}, 弱引用 = {}", Rc::strong_count(&leaf), Rc::weak_count(&leaf));
// leaf 强引用 = 1, 弱引用 = 0,同样由于上面作用域的销毁,branch对于leaf不再强引用。
所以当我们的数据类型有循环引用关系的时候便可以使用Weak<T>类型,使相互引用的数据在指向彼此的同时避免产生循环引用和内存泄漏。