Rust很难制造内存泄漏(memory leak),但并不是不可能,使用Rc
制造引用循环🔗
考虑下面代码:
use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug)]
enum List {
Cons(i32, RefCell<Rc<List>>),
Nil,
}
impl List {
fn tail(&self) -> Option<&RefCell<Rc<List>>> {
match self {
Cons(_, item) => Some(item),
Nil => None,
}
}
}
fn main() {
let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));
println!("a initial rc count = {}", Rc::strong_count(&a));
println!("a next item = {:?}", a.tail());
let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));
println!("a rc count after b creation = {}", Rc::strong_count(&a));
println!("b initial rc count = {}", Rc::strong_count(&b));
println!("b next item = {:?}", b.tail());
if let Some(link) = a.tail() {
*link.borrow_mut() = Rc::clone(&b);
}
println!("b rc count after changing a = {}", Rc::strong_count(&b));
println!("a rc count after changing a = {}", Rc::strong_count(&a));
// Uncomment the next line to see that we have a cycle;
// it will overflow the stack
// println!("a next item = {:?}", a.tail());
}
这里Cons的第二项是RefCell<Rc<List>>,这里就可以修改Cons指向的List,并且增加了个tail方法来获取Cons的第二项。
在main函数中,我们创建了一个Cons,它的第二项指向了Nil,然后我们创建了一个Cons,它的第二项指向了第一个Cons,后面我们修改了第一个Cons的第二项,使其指向了第二个Cons,这样就形成了一个引用循环。
运行结果:
a initial rc count = 1
a next item = Some(RefCell { value: Nil })
a rc count after b creation = 2
b initial rc count = 1
b next item = Some(RefCell { value: Cons(5, RefCell { value: Nil }) })
b rc count after changing a = 2
a rc count after changing a = 2
可以看到在将第一个Cons的第二项指向第二个Cons后,两个列表的引用计数都是2,在最后,Rust丢弃b,这时第二个实例的引用计数减为1,然后Rust丢弃a,这时第一个实例的引用计数减为1,但是由于这两个实例的引用计数不为0,所以第二个实例不会被释放,这样就形成了一个引用循环。
如果我们取消代码后面注释的println!,Rust就会打印a指向b,然后b指向a,这样就会一直循环下去,直到栈溢出。
如果在更为复杂的程序中的循环里分配了很多内存并占有很长时间,这个程序会使用多余它所需要的内存,有可能压垮系统并造成没有内存可供使用。
对于循环引用,除了使用自动化测试、代码评审等使其最小化,也可以重新组织数据结构,让一部分引用有所有权而另一部分没有,这样只有有所有权的才能影响值是否被丢弃。
将Rc变为Weak🔗
考虑以下代码:
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug)]
struct Node {
value: i32,
children: RefCell<Vec<Rc<Node>>>,
}
fn main() {
let leaf = Rc::new(Node {
value: 3,
children: RefCell::new(vec![]),
});
let branch = Rc::new(Node {
value: 5,
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
}
这里定义了一个Node结构体,它有一个值和一个子节点的向量,子节点是Rc<Node>,这样就可以创建一个树,并拥有子节点的所有权,也可以更改子节点。
接下来,我们创建了一个leaf节点和一个branch节点,leaf节点没有子节点,branch节点有一个子节点leaf。
这个时候,我们需要让leaf节点知道branch是他的父节点。
增加子到父的引用🔗
我们需要在Node结构体中增加一个parent字段,这个字段如果是RefCell<Rc<Node>>,就可以存储父节点的引用,但是会形成循环引用。现在换一个思路,如果父节点被丢弃,则子节点也应该被丢弃,如果丢弃子节点,其父节点应该依然存在,这便是弱引用的例子。
我们可以使用Weak<T>来存储父节点的引用,这样就可以避免循环引用:
use std::cell::RefCell;
use std::rc::{Rc, Weak};
#[derive(Debug)]
struct Node {
value: i32,
parent: RefCell<Weak<Node>>,
children: RefCell<Vec<Rc<Node>>>,
}
fn main() {
let leaf = Rc::new(Node {
value: 3,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});
println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
let branch = Rc::new(Node {
value: 5,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);
println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
}
这样子节点就能引用父节点而不拥有父节点,并且使用upgrade方法可以获得包含父节点引用的Option类型。第一个打印时leaf并没有父节点,所以输出为:
leaf parent = None
第二个打印时leaf有父节点,而Weak<Node>会被打印为(Weak),避免了栈溢出的循环,所以输出为:
leaf parent = Some(Node { value: 5, parent: RefCell { value: (Weak) }, children: RefCell { value: [Node { value: 3, parent: RefCell { value: (Weak) }, children: RefCell { value: [] } }] } })可视化strong_count和weak_count的改变🔗
考虑下面代码:
use std::cell::RefCell;
use std::rc::{Rc, Weak};
#[derive(Debug)]
struct Node {
value: i32,
parent: RefCell<Weak<Node>>,
children: RefCell<Vec<Rc<Node>>>,
}
fn main() {
let leaf = Rc::new(Node {
value: 3,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});
println!(
"leaf strong = {}, weak = {}",
Rc::strong_count(&leaf),
Rc::weak_count(&leaf),
);
{
let branch = Rc::new(Node {
value: 5,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);
println!(
"branch strong = {}, weak = {}",
Rc::strong_count(&branch),
Rc::weak_count(&branch),
);
println!(
"leaf strong = {}, weak = {}",
Rc::strong_count(&leaf),
Rc::weak_count(&leaf),
);
}
println!("leaf parent = {:?}",leaf.parent.borrow().upgrade());
println!(
"leaf strong = {}, weak = {}",
Rc::strong_count(&leaf),
Rc::weak_count(&leaf),
);
}
一开始创建了leaf,它的强引用计数为1,弱引用计数为0,后面内部作用域创建branch后,branch的强引用计数为1,弱引用计数为1,leaf的强引用计数为2,弱引用计数为0,后面离开内部作用域,branch的强引用计数为0,被丢弃,leaf的强引用计数为1,弱引用计数为0。