标题
- 五、引用计数智能指针
- 5.1 共享引用计数智能指针共享数据
- 5.2 使用Box定义三个共享链表
- 5.3 使用Rc代替Box
- 5.4 引用计数增加实验
- 六、RefCell和内部可变性模式
- 6.1 通过RefCell在运行时检查借用规则
- 6.2 内部可变性:不可变值的可变借用
- 1)内部可变性的用例:mock对象
- 2)创建测试场景
- 6.3 RefCell在运行时记录借用
- 6.4 结合Rc和RefCell拥有多个可变数据所有者
- 七、引用循环会导致内存泄漏
- 7.1 概念
- 7.2 制造引用循环
- 7.3 解决方案
- 1)避免引用循环:将Rc变成Weak
- 2)Strong VS Weak
- 3)创建带有父子结点的树形数据结构
五、引用计数智能指针
- 引用计数
Rc<T>
通过引入的数量记录着某个变量是否仍在被使用; - 如果引用计数为0,则代表没有任何有效引用,此时可以被清理;
Rc<T>
用于堆上分配的内存被程序的多个部分读取,且无法确定哪一部分最后结束使用;Rc<T>
只能用于单线程;
5.1 共享引用计数智能指针共享数据
- 创建两个共享第三个列表所有权的示例
- 列表
a
从5开始; - 列表
b
和c
分别从3和4开始,然后共享a的部分;
5.2 使用Box定义三个共享链表
enum List {Cons(i32, Box<List>),Nil,
}use crate::List::{Cons, Nil};fn main() {let a = Cons(5,Box::new(Cons(10,Box::new(Nil))));let b = Cons(3, Box::new(a));let c = Cons(4, Box::new(a));
}
- 代码无法编译通过;
- 当创建列表b时,a的所有权被移进了b;
- 当创建列表c时,a的所有权已经被移动,所以c无法创建成功;
5.3 使用Rc代替Box
enum List {Cons(i32, Rc<List>),Nil,
}use std::rc::Rc;
use crate::List::{Cons, Nil};fn main() {let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));let b = Cons(3, Rc::clone(&a));let c = Cons(4, Rc::clone(&a));
}
- 在List中,使用
Rc<T>
代替Box<T>
; - 创建b和c时,克隆a所包含的
Rc<List>
,这使引用计数分别加1并允许a和b以及a和c之间共享Rc<List>
中数据的所有权; - 也可以将
Rc::clone(&a)
换成a.clone()
,Rc::clone
只会增加引用计数而不会执行深拷贝;
5.4 引用计数增加实验
- 打印引用计数值,以便直接看着引用计数的变化;
fn main() {let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));println!("count after creating a = {}", Rc::strong_count(&a));let b = Cons(3, Rc::clone(&a));println!("count after creating b = {}", Rc::strong_count(&a));{let c = Cons(4, Rc::clone(&a));println!("count after creating c = {}", Rc::strong_count(&a));}println!("count after c goes out of scope = {}", Rc::strong_count(&a));
}
- 运行结果如下
- 初始计数为1,每调用一个克隆都会使计数加1;
- 离开作用域后会自动减少计数;
通过不可变引用,
Rc<T>
允许在程序的多个部分之间只读的共享数据;
六、RefCell和内部可变性模式
- 内部可变性(Interior mutability)是Rust的一个设计模式,可让使用者在有不可变引用时也可以改变数据(这通过是规则所禁止的);
- 为了改变数据,该模式在数据结构中使用
unsafe
代码来模糊Rust通常的可变性和借用规则; - 当可以确保代码在运行时会遵守借用规则,即使编译器不能保证的情况,可以选择使用那些运用内部可变性模式的类型。所涉及的unsafe代码将被封装进安全的API中,而外部类型仍然是不可变的;
6.1 通过RefCell在运行时检查借用规则
RefCell<T>
代表其数据的唯一所有权;RefCell<T>
的借用规则的不可变性作用于运行时;RefCell<T>
只能用于单线程场景;
6.2 内部可变性:不可变值的可变借用
- 当有不可变值时,不能可变地借用它;
- 如下的编译报错
fn main() {let x = 5;let y = &mut x;
}
- 可以在特定情况下,令一个值在其方法内部能够修改自身,而在其他代码中仍然视为不可变;
RefCell<T>
是一个获得内部可变性的方法;
1)内部可变性的用例:mock对象
- 测试替身(test double) 代表一个测试中替代某个类型的类型;
- mock对象是特定类型的测试替身,它们记录测试过程中发生了什么以便可以说明操作是否正确;
- Rust没有内建mock对象功能,可以自己创建一个与mock对象有着相同功能的结构体;
2)创建测试场景
- 编写一个记录某个值与最大值的差距的库;
- 根据当前值与最大值的差距来发送消息;
- 例如:可以记录用户所允许的API调用数量限额;
src/lib.rs
pub trait Messenger {fn send(&self, msg: &str);
}pub struct LimitTracker<'a, T: Messenger> {messenger: &'a T,value: usize,max: usize,
}impl<'a, T> LimitTracker<'a, T>where T: Messenger {pub fn new(messenger: &T, max: usize) -> LimitTracker<T> {LimitTracker {messenger,value: 0,max,}}pub fn set_value(&mut self, value: usize) {self.value = value;let percentage_of_max = self.value as f64 / self.max as f64;if percentage_of_max >= 1.0 {self.messenger.send("Error: You are over your quota!");} else if percentage_of_max >= 0.9 {self.messenger.send("Urgent warning: You've used up over 90% of your quota!");} else if percentage_of_max >= 0.75 {self.messenger.send("Warning: You've used up over 75% of your quota!");}}
}
- Message trait拥有send方法,这是定义的mock对象所需要拥有的接口;
- set_value方法可以改变传递的value参数的值;
#[cfg(test)]
mod tests {use super::*;struct MockMessenger {sent_messages: Vec<String>,}impl MockMessenger {fn new() -> MockMessenger {MockMessenger { sent_messages: vec![] }}}impl Messenger for MockMessenger {fn send(&self, message: &str) {self.sent_messages.push(String::from(message));}}#[test]fn it_sends_an_over_75_percent_warning_message() {let mock_messenger = MockMessenger::new();let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);limit_tracker.set_value(80);for item in &mock_messenger.sent_messages{println!("{}", item);}assert_eq!(mock_messenger.sent_messages.len(), 1);}
}
- 测试代码定义了一个MockMessenger结构体,其要发送的字段为一个String值的Vec;
- 为MockMessenger结构体实现Messenger trait,这样就可以为LimitTracker提供一个MockMessenger;
- 测试用例中
- 创建一个MockMessager实例,send_message存储着空vector;
- 创建LimitTracker,传入MockMessenger实例和最大值100;
- 调用LimitTracker的set_value方法,并传入80,这将运行
self.messenger.send("Warning: You've used up over 75% of your quota!");
代码; - 最后断言sent_messages的长度为1,打印其值;
编译报错
- 由于send方法获取了self的不可变引用,因此不能修改MockMessenger来记录消息;
- 也不能按照信息使用
&mut self
替代,否则send的参数与Messenger trait的参数不符合; - 将这两项也修改;
pub trait Messenger {fn send(&mut self, msg: &str);
}impl Messenger for MockMessenger {fn send(&mut self, message: &str) {self.sent_messages.push(String::from(message));}
}
- 还是如下报错;
- 这就使得内部可变性有了用武之地!
- 通过
RefCell
来储存sent_messages就可以解决这个问题;
修改的部分如下,采用注释进行对比
#[cfg(test)]
mod tests {use super::*;use std::cell::RefCell;struct MockMessenger {// sent_messages: Vec<String>,sent_messages: RefCell<Vec<String>>,}impl MockMessenger {fn new() -> MockMessenger {// MockMessenger { sent_messages: vec![] }MockMessenger { sent_messages: RefCell::new(vec![])}}}impl Messenger for MockMessenger {fn send(&self, message: &str) {// self.sent_messages.push(String::from(message));self.sent_messages.borrow_mut().push(String::from(message));}}#[test]fn it_sends_an_over_75_percent_warning_message() {let mock_messenger = MockMessenger::new();let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);limit_tracker.set_value(80);for item in mock_messenger.sent_messages.borrow().iter(){println!("{}", item);}assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);}
}
- 将
sent_messages
字段的类型由Vec<String>
改为RefCell<Vec<String>>
; - 在new函数中新建了一个
RefCell<Vec<String>>
实例替代空vector; - 在send方法中,使用RefCell的borrow_mut方法获取RefCell中值的可变引用(是一个vector);
- 最后的打印以及断言中,使用RefCell的borrow以获取vector的不可变引用;
- 最后输出了vector中的字符串并通过了测试;
6.3 RefCell在运行时记录借用
- 一般情况下使用
&
和&mut
分别创建不可变和可变引用; - 对于
RefCell<T>
,分别使用borrow
和borrow_mut
方法创建不可变和可变引用; - borrow方法返回
Ref<T>
类型的智能指针,borrow_mut
方法返回RefMut<T>
类型的智能指针; RefCell<T>
记录当前有多少个活动的Ref<T>
和RefMut<T>
智能指针;- 每次调用borrow,
RefCell<T>
将活动的不可变借用计数加1,当Ref<T>
值离开作用域时,不可变借用计数减1; RefCell<T>
的实现会在运行时出现panic;
impl Messenger for MockMessenger {fn send(&self, message: &str) {let mut one_borrow = self.sent_messages.borrow_mut();let mut two_borrow = self.sent_messages.borrow_mut();one_borrow.push(String::from(message));two_borrow.push(String::from(message));}
}
- 代码在send函数里相同的作用域创建两个可变借用;
- 编译能正常通过,运行测试时失败;
- 错误提示already borrowed: BorrowMutError,就是
RefCell<T>
在运行时处理违反借用规则时的报错;
6.4 结合Rc和RefCell拥有多个可变数据所有者
RefCell<T>
的一个常见用法是与Rc<T>
结合;Rc<T>
允许对相同数据有多个所有者,只能提供数据的不可变访问;- 一个储存了
RefCell<T>
的Rc<T>
,可以得到有多个所有者且可以修改的值;
#[derive(Debug)]
enum List {Cons(Rc<RefCell<i32>>, Rc<List>),Nil,
}use crate::List::{Cons, Nil};
use std::rc::Rc;
use std::cell::RefCell;fn main() {let value = Rc::new(RefCell::new(5));let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));let b = Cons(Rc::new(RefCell::new(6)), Rc::clone(&a));let c = Cons(Rc::new(RefCell::new(10)), Rc::clone(&a));*value.borrow_mut() += 10;println!("a after = {:?}", a);println!("b after = {:?}", b);println!("c after = {:?}", c);
}
- 创建
Rc<RefCell<i32>>
实例并存储在变量value中; - 在a中用包含value的Cons成员创建了一个List;
- 克隆value后a和value 以便a和value都能拥有其内部值 5 的所有权;
- 将列表a封装进
Rc<T>
,当创建列表b和c时,他们都可以引用a; - 对value调用borrow_mut解引用
Rc<T>
以获取内部的RefCell<T>
值; - borrow_mut方法返回
RefMut<T>
智能指针,可以对其使用解引用运算符并修改其内部值;
- 输出a,b,c时,可以看到他们都拥有修改后的值15;
- 通过使用
RefCell<T>
可以拥有一个表面上不可变的List; - 可以使用
RefCell<T>
中提供内部可变性的方法来在需要时修改数据; RefCell<T>
的运行时借用规则检查也确实保护我们免于出现数据竞争;
标准库中也有其他提供内部可变性的类型,比如 Cell,它类似 RefCell 但有一点除外:它并非提供内部值的引用,而是把值拷贝进和拷贝出 Cell。还有 Mutex,其提供线程间安全的内部可变性。
七、引用循环会导致内存泄漏
7.1 概念
- Rust的内存安全可以保证很难发生内存泄露,但并非完成不可能!
- 使用
Rc<T>
和RefCell<T>
创造出循环引用,引用数量不会减少为0,就会发生内存泄漏;
7.2 制造引用循环
use std::rc::Rc;
use std::cell::RefCell;
use crate::List::{Cons, Nil};#[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函数中创建了一个链表以及一个指向a中链表的b链表;
- 在
if let
语句中使用a.tail()
取出a的第二个元素,将它指向b,形成一个引用循环:两个List互相指向彼此;
- 开始创建a时的引用数量为1,a的下一个元素是Nil;
- b创建之后a的引用个数变为2,b的引用数量为1,b的下一个元素就是a;
- 修改a的第二个元素的指向后,a和b都有两个引用。
- 将main函数的最后一个
println!
的注释取消,就会发现在循环打印;
7.3 解决方案
- 开发者采用自动化测试、代码评审等;
- 重新组织数据结构,使得一部分引用拥有所有权而另一部分没有;
1)避免引用循环:将Rc变成Weak
Rc::clone
为Rc<T>
实例的strong_count加1,Rc<T>
的实例只有在strong_count为0时才会被清理;Rc<T>
实例通过调用Rc::downgrade
方法可以创建值的Weak Reference
(弱引用);- 调用
Rc::downgrade
时会得到Weak<T>
类型的智能指针; - 调用
Rc::downgrade
时会将weak_count加1; - weak_count无需计数为0就能使
Rc<T>
实例被清理;
2)Strong VS Weak
- 强引用(Strong Reference)是关于如何共享
Rc<T>
实例的所有权; - 弱引用(Weak Reference)并不表示所有权关系;
- 当强引用数量为0时,弱引用会自动断开,因此弱引用不会创建循环引用;
- 在
Weak<T>
实例上调用upgrade方法,返回Option<Rc<T>>
,以此保证指向弱引用的值仍然存在;
3)创建带有父子结点的树形数据结构
加入子节点
use std::rc::Rc;
use std::cell::RefCell;#[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>
保证了所有的子结点共享所有权;RefCell
保证了能修改其他节点的子结点;
- 在main函数中:
- 创建了值为3且没有子节点的Node实例
leaf
; - 创建了值为5且以
leaf
作为子节点的实例branch
; - 这就可以通过
branch.children
从branch中获得leaf,反过来不行(可以通过添加parent解决);
- 创建了值为3且没有子节点的Node实例
加入父结点
- 要使能够从leaf中获得branch,需要再加一个parent;
- parent的类型如果是
Rc<T>
则会产生循环引用; - 换个思路:
- 父节点应该拥有子节点:父节点被销毁了,子节点也应该被销毁;
- 子节点不应该拥有父节点:子节点被销毁,父节点应该依然存在;
- 因此应该使用弱引用类型
Weak<T>
,具体的是RefCell<Weak<Node>>
;
use std::rc::{Rc, Weak};
use std::cell::RefCell;#[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());
}
- 对应Node结构体
- 添加引向父节点的结构
parent: RefCell::new(Weak::new()),
;
- 添加引向父节点的结构
- 对于main函数
- 创建
leaf
结点:值为3,由于没有父结点,因此创建了空的weak引用实例; - 创建
branch
结点:值为5,父结点也为空,leaf
仍作为子结点; - 第一个打印语句通过父结点的
borrow()
方法获取不可变引用,然后用upgrade()
方法打印出来(为空); - 通过
leaf.parent.borrow_mut()
获取leaf
的父结点的可变引用,然后通过解引用将其指向branch
; - 接着打印leaf的父结点,没有无限的输出也表示没有造成循环引用;
- 创建
可视化strong_count和weak_count的改变
- 创建新的内部作用域并放入branch的创建;
- 观察
Rc<Node>
实例的strong_count和weak_count值变化;
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
之后,其Rc<Node>
的强引用为1,没有弱引用; - 进入内部作用域
- branch的强引用计数为1,弱引用计数也为1(leaf.parnet引向了branch);
- leaf的弱引用计数为2(一个是本身的,另一个是branch的branch.children储存了leaf的
Rc<Node>
的拷贝),弱引用计数仍然为0;
- 离开内部作用域后
- branch的作用域也就结束了,leaf的
Rc<Node>
强引用减少为0,因此相应的branch被丢弃; - 来自
leaf.parent
的弱引用计数为1,这与Node是否被丢弃无关,因此没有产生任何内存泄漏!
- branch的作用域也就结束了,leaf的
- 离开内部作用域后访问
leaf
的父节点,再次得到None。 - 到程序结尾,由于leaf又是
Rc<Node>
唯一的引用了,因此最后打印leaf
中Rc<Node>
的强引用计数为1,弱引用计数为0;
管理计数和值的逻辑都内建于
Rc<T>
和Weak<T>
以及它们的Drop trait
实现中。通过在Node定义中指定从子节点到父节点的关系为一个Weak<T>
引用,就能够拥有父节点和子节点之间的双向引用而不会造成引用循环和内存泄漏。