写点什么

5 分钟速读之 Rust 权威指南(二十九)循环引用

用户头像
码生笔谈
关注
发布于: 36 分钟前
5分钟速读之Rust权威指南(二十九)循环引用

循环引用

对于前端同学来说,了解 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>来防止循环引用:


// 引入Weakuse 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>类型,使相互引用的数据在指向彼此的同时避免产生循环引用和内存泄漏。

发布于: 36 分钟前阅读数: 3
用户头像

码生笔谈

关注

欢迎关注「码生笔谈」公众号 2018.09.09 加入

前端、Rust干货分享,一起成为更好的Javascripter & Rustacean

评论

发布
暂无评论
5分钟速读之Rust权威指南(二十九)循环引用