Cell和RefCell
- Cell
- RefCell
- RefCell在运行时记录借用
- 结合Rc和RefCell来拥有多个可变数据所有者
- 引用循环与内存泄漏
- 制造引用循环
- 避免引用循环:将Rc变为Weak
- 创建树形数据结构:带子节点的Node
- 增加从子到父的引用
- 可视化strong_count 和 weak_count 的改变
Rust 提供了 Cell 和 RefCell 用于内部可变性,简而言之,可以在拥有不可变引用的同时修改目标数据,对于正常的代码实现来说,这个是不可能做到的(要么一个可变借用,要么多个不可变借用)。
内部可变性的实现是因为 Rust 使用了 unsafe 来做到这一点,但是对于使用者来说,这些都是透明的,因为这些不安全代码都被封装到了安全的 API 中
Cell
Cell 和 RefCell 在功能上没有区别,区别在于 Cell< T > 适用于 T 实现 Copy 的情况:
use std::cell::Cell;
fn main() {let c = Cell::new("asdf");let one = c.get();c.set("qwer");let two = c.get();println!("{},{}", one, two);asdf,qwer
}
以上代码展示了 Cell 的基本用法,有几点值得注意:
- “asdf” 是 &str 类型,它实现了 Copy 特征
- c.get 用来取值,c.set 用来设置新值
取到值保存在 one 变量后,还能同时进行修改,这个违背了 Rust 的借用规则,但是由于 Cell 的存在,我们很优雅地做到了这一点,但是如果你尝试在 Cell 中存放String:
let c = Cell::new(String::from("asdf"));
编译器会立刻报错,因为 String 没有实现 Copy 特征
RefCell
- Rc< T > 允许相同数据有多个所有者;Box< T > 和 RefCell 有单一所有者。
- Box< T > 允许在编译时执行不可变或可变借用检查;Rc< T > 仅允许在编译时执行不可变借用检查;RefCell< T > 允许在运行时执行不可变或可变借用检查。
- 因为 RefCell< T > 允许在运行时执行可变借用检查,所以我们可以在即便 RefCell< T > 自身是不可变的情况下修改其内部的值。
举个例子
pub trait Messenger {fn send(&self, msg: &str);
}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));}
}
这段代码不会编译成功,因为MockMessenger用的是不可变引用,所以它内部的vec也是不可变的,所以不能push。修改trait的签名是一个好的方法,但是如果不能修改呢?
struct MockMessenger {sent_messages: RefCell<Vec<String>>,}impl MockMessenger {fn new() -> MockMessenger {MockMessenger {sent_messages: RefCell::new(vec![]),}}}impl Messenger for MockMessenger {fn send(&self, message: &str) {self.sent_messages.borrow_mut().push(String::from(message));}}
对于 send 方法的实现,第一个参数仍为 self 的不可变借用,这是符合方法定义的。我们调用 self.sent_messages 中 RefCell 的 borrow_mut 方法来获取 RefCell 中值的可变引用,这是一个 vector。接着可以对 vector 的可变引用调用 push 以便记录测试过程中看到的消息。
RefCell在运行时记录借用
当创建不可变和可变引用时,我们分别使用 & 和 &mut 语法。对于 RefCell< T> 来说,则是 borrow 和 borrow_mut 方法,这属于 RefCell< T> 安全 API 的一部分。borrow 方法返回 Ref< T> 类型的智能指针,borrow_mut 方法返回 RefMut< T> 类型的智能指针。这两个类型都实现了 Deref,所以可以当作常规引用对待。
RefCell< T > 记录当前有多少个活动的 Ref< T > 和 RefMut< T > 智能指针。每次调用 borrow,RefCell< T > 将活动的不可变借用计数加一。当 Ref< T > 值离开作用域时,不可变借用计数减一。就像编译时借用规则一样,RefCell< T > 在任何时候只允许有多个不可变借用或一个可变借用。
如果我们尝试违反这些规则,相比引用时的编译时错误,RefCell< T > 的实现会在运行时出现 panic。
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>
whereT: Messenger,
{pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, 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!");}}
}#[cfg(test)]
mod tests {use super::*;use std::cell::RefCell;struct MockMessenger {sent_messages: RefCell<Vec<String>>,}impl MockMessenger {fn new() -> MockMessenger {MockMessenger {sent_messages: RefCell::new(vec![]),}}}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));}}#[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);assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);}
}
fn main(){}
上面这个代码不会出现编译错误,但是cargo test时会panic
结合Rc和RefCell来拥有多个可变数据所有者
回忆一下 Rc< T> 允许对相同数据有多个所有者,不过只能提供数据的不可变访问。如果有一个储存了 RefCell< T> 的 Rc< T> 的话,就可以得到有多个所有者 并且 可以修改的值了!
#[derive(Debug)]
enum List {Cons(Rc<RefCell<i32>>, Rc<List>),Nil,
}use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;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(3)), Rc::clone(&a));let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a));*value.borrow_mut() += 10;println!("a after = {:?}", a);println!("b after = {:?}", b);println!("c after = {:?}", c);
}
----------------------------------------------
$ cargo runCompiling cons-list v0.1.0 (file:///projects/cons-list)Finished dev [unoptimized + debuginfo] target(s) in 0.63sRunning `target/debug/cons-list`
a after = Cons(RefCell { value: 15 }, Nil)
b after = Cons(RefCell { value: 3 }, Cons(RefCell { value: 15 }, Nil))
c after = Cons(RefCell { value: 4 }, Cons(RefCell { value: 15 }, Nil))
这里创建了一个 Rc<RefCell< i32>> 实例并储存在变量 value 中以便之后直接访问。接着在 a 中用包含 value 的 Cons 成员创建了一个 List。需要克隆 value 以便 a 和 value 都能拥有其内部值 5 的所有权,而不是将所有权从 value 移动到 a 或者让 a 借用 value。
需要克隆value,以便拥有内部值5的所有权,而不是移动所有权或借用。如果转移所有权,value就用不了了。这里的克隆是浅拷贝,仅仅复制了智能指针并增加了引用计数,并没有克隆底层数据。
Rc与Arc实现1vN所有权机制
通过使用 RefCell< T>,我们可以拥有一个表面上不可变的 List,不过可以使用 RefCell< T> 中提供内部可变性的方法来在需要时修改数据。RefCell< T> 的运行时借用规则检查也确实保护我们免于出现数据竞争——有时为了数据结构的灵活性而付出一些性能是值得的。注意 RefCell< T> 不能用于多线程代码!Mutex< T> 是一个线程安全版本的 RefCell< T> 。
引用循环与内存泄漏
Rust 的内存安全性保证使其难以意外地制造永远也不会被清理的内存(被称为 内存泄漏(memory leak)),但并不是不可能。Rust 并不保证完全防止内存泄漏,这意味着内存泄漏在 Rust 中被认为是内存安全的。这一点可以通过 Rc< T> 和 RefCell< T> 看出:创建引用循环的可能性是存在的。这会造成内存泄漏,因为每一项的引用计数永远也到不了 0,持有的数据也就永远不会被释放。
简单来说,就是,变量超出作用域,或者main函数快结束的时候,Rust会丢弃变量,将其引用计数减1,但是只有为0了,才会被回收!!!
制造引用循环
List 定义的另一种变体。现在 Cons 成员的第二个元素是 RefCell<Rc< List >>
这里还增加了一个 tail 方法来方便我们在有 Cons 成员的时候访问其第二项。
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());
}
--------------------------------------------
$ cargo runCompiling cons-list v0.1.0 (file:///projects/cons-list)Finished dev [unoptimized + debuginfo] target(s) in 0.53sRunning `target/debug/cons-list`
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
这里在变量 a 中创建了一个 Rc< List > 实例来存放初值为 5, Nil 的 List 值。接着在变量 b 中创建了存放包含值 10 和指向列表 a 的 List 的另一个 Rc< List > 实例。
最后,修改 a 使其指向 b 而不是 Nil,这就创建了一个循环。为此需要使用 tail 方法获取 a 中 RefCell<Rc< List >> 的引用,并放入变量 link 中。接着使用 RefCell<Rc< List >> 的 borrow_mut 方法将其值从存放 Nil 的 Rc< List > 修改为 b 中的 Rc< List >。
可以看到将列表 a 修改为指向 b 之后, a 和 b 中的 Rc< List > 实例的引用计数都是 2。在 main 的结尾,Rust 丢弃 b,这会使 b Rc< List > 实例的引用计数从 2 减为 1。然而,b Rc< List> 不能被回收,因为其引用计数是 1 而不是 0。接下来 Rust 会丢弃 a 将 a Rc< List > 实例的引用计数从 2 减为 1。这个实例也不能被回收,因为 b Rc< List > 实例依然引用它,所以其引用计数是 1。这些列表的内存将永远保持未被回收的状态。
如果取消最后 println! 的注释并运行程序,Rust 会尝试打印出 a 指向 b 指向 a 这样的循环直到栈溢出。
避免引用循环:将Rc变为Weak
创建树形数据结构:带子节点的Node
我们希望Node能够拥有其子节点,同时也希望能将所有权共享给变量,以便可以直接访问树中的每一个Node,为此Vec的项的类型被定义为Rc< Node >。我们还希望能够修改其他节点的子节点,因此Children中的Vec被放进了RefCell。
创建leaf,值为3,创建branch,值为5,子结点为leaf。
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)]),});
}这里克隆了 leaf 中的 Rc<Node> 并储存在 branch 中,这意味着 leaf 中的 Node 现在有两个所有者:leaf和branch。
可以通过 branch.children 从 branch 中获得 leaf,不过无法从 leaf 到 branch。
leaf 没有到 branch 的引用且并不知道它们相互关联。我们希望 leaf 知道 branch 是其父节点。稍后我们会这么做。
增加从子到父的引用
为了使子节点知道其父节点,需要在 Node 结构体定义中增加一个 parent 字段。问题是 parent 的类型应该是什么。我们知道其不能包含 Rc< T >,因为这样 leaf.parent 将会指向 branch 而 branch.children 会包含 leaf 的指针,这会形成引用循环,会造成其 strong_count 永远也不会为 0。
现在换一种方式思考这个关系,父节点应该拥有其子节点:如果父节点被丢弃了,其子节点也应该被丢弃。然而子节点不应该拥有其父节点:如果丢弃子节点,其父节点应该依然存在。这正是弱引用的例子!
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());
}
-----------------------------------------
leaf parent = None
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并与leaf关联,此时branch中的Rc< Node>强引用计数为1,弱引用计数为1(Rc::downgrade(&branch);)。同时leaf的强引用计数变为2,因为现在 branch 的 branch.children 中储存了 leaf 的 Rc< Node> 的拷贝,不过弱引用计数仍然为 0。
当内部作用域结束时,branch 离开作用域,其Rc< Node> 的强引用计数减少为 0,所以其 Node 被丢弃。来自 leaf.parent 的弱引用计数 1 与 Node 是否被丢弃无关,所以并没有产生任何内存泄漏!。
branch 离开作用域,Rc< Node> 的强引用计数减少为 0,所以其 Node 被丢弃。来自 leaf.parent 的弱引用计数 1 与 Node 是否被丢弃无关,所以并没有产生任何内存泄漏。
parent弱引用,children强引用。