Cell和RefCell

Cell和RefCell

    • Cell
    • RefCell
    • RefCell在运行时记录借用
    • 结合Rc和RefCell来拥有多个可变数据所有者
    • 引用循环与内存泄漏
      • 制造引用循环
      • 避免引用循环:将Rc变为Weak
      • 创建树形数据结构:带子节点的Node
      • 增加从子到父的引用
      • 可视化strong_count 和 weak_count 的改变

Rust 提供了 CellRefCell 用于内部可变性,简而言之,可以在拥有不可变引用的同时修改目标数据,对于正常的代码实现来说,这个是不可能做到的(要么一个可变借用,要么多个不可变借用)。

内部可变性的实现是因为 Rust 使用了 unsafe 来做到这一点,但是对于使用者来说,这些都是透明的,因为这些不安全代码都被封装到了安全的 API 中

Cell

CellRefCell 在功能上没有区别,区别在于 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_messagesRefCellborrow_mut 方法来获取 RefCell 中值的可变引用,这是一个 vector。接着可以对 vector 的可变引用调用 push 以便记录测试过程中看到的消息。

RefCell在运行时记录借用

当创建不可变和可变引用时,我们分别使用 &&mut 语法。对于 RefCell< T> 来说,则是 borrowborrow_mut 方法,这属于 RefCell< T> 安全 API 的一部分。borrow 方法返回 Ref< T> 类型的智能指针,borrow_mut 方法返回 RefMut< T> 类型的智能指针。这两个类型都实现了 Deref,所以可以当作常规引用对待。

RefCell< T > 记录当前有多少个活动的 Ref< T >RefMut< T > 智能指针。每次调用 borrowRefCell< 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 中用包含 valueCons 成员创建了一个 List。需要克隆 value 以便 avalue 都能拥有其内部值 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强引用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/176888.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Git忽略文件.gitignore的使用

1.为什么使用? 当你使用git add .的时候有没有遇到把你不想提交的文件也添加到了缓存中去&#xff1f;比如项目的本地配置信息&#xff0c;如果你上传到Git中去其他人pull下来的时候就会和他本地的配置有冲突&#xff0c;所以这样的个性化配置文件我们一般不把它推送到git服务…

1334. 阈值距离内邻居最少的城市

分析题目两点“阈值距离”、“邻居最少”。 “阈值距离”相当于定了个上界&#xff0c;求节点之间的最短距离。 “邻居最少”相当于能连接的点的数量。 求节点之间的最短距离有以下几种方法&#xff1a; 在这道题当中&#xff0c;n的范围是100以内&#xff0c;所以可以考虑O(n…

Jetson orin部署大模型示例教程

一、LLM介绍 LLM指的是Large Language Model&#xff08;大型语言模型&#xff09;&#xff0c;是一类基于深度学习的自然语言处理技术&#xff0c;其主要目的是让机器能够更好地理解和生成人类的自然语言文本&#xff0c;如文章、对话、搜索等。 教程 - text-generation-web…

Jenkins入门——安装docker版的Jenkins 配置mvn,jdk等 使用案例初步 遇到的问题及解决

前言 Jenkins是开源CI&CD软件领导者&#xff0c; 提供超过1000个插件来支持构建、部署、自动化&#xff0c; 满足任何项目的需要。 官网&#xff1a;https://www.jenkins.io/zh/ 本篇博客介绍docker版的jenkins的安装和使用&#xff0c;maven、jdk&#xff0c;汉语的配置…

都dubbo3了,别再用xml了配置dubbo服务了

这里是weihubeats,觉得文章不错可以关注公众号小奏技术&#xff0c;文章首发。拒绝营销号&#xff0c;拒绝标题党 背景 最近项目再准备升级dubbo3,所以打算简单实现一个dubbo3的demo。 来学习一下dubbo dubbo3 dubbo3主要是为了融入云原生打造的 Dubbo 3 提供的核心特性列…

【C#学习】给FormClosing增加消息响应函数

第一步&#xff1a;增加消息句柄 第二步&#xff1a;编写消息函数 private void Form1_FormClosing(object sender, FormClosingEventArgs e) {//add your code hereserialPort1.Close();}

2023深圳高交会,11月15-19日在深圳会展中心盛大开幕,展出五天时间

跨越山海&#xff0c;共赴鹏城。 11月15日至19日&#xff0c;第二十五届中国国际高新技术成果交易会在深圳会展中心&#xff08;福田展区&#xff09;和深圳国际会展中心&#xff08;宝安展区&#xff09;两馆同时举行。一场不可错过的全球性高科技盛会如期而至。 科技赋能发…

常见限流算法解读

目录 前言 固定窗口&#xff08;计算器法&#xff09; 滑动窗口 漏桶算法 令牌桶算法 总结 前言 在现在的互联网系统中有很多业务场景&#xff0c;比如商品秒杀、下单、数据查询详情&#xff0c;其最大特点就是高并发&#xff0c;但是我们的系统通常不能承受这么大的流…

解决 requests.post 数据字段编码问题的方法

问题背景 在进行网络请求时&#xff0c;我们通常会使用requests库的post方法来发送POST请求。然而&#xff0c;当我们尝试发送包含特殊字符&#xff08;如中文字符&#xff09;的数据时&#xff0c;可能会遇到数据字段被编码的问题。这可能会导致请求失败或者服务器无法正确解…

Ubuntu环境下为串口设置别名

本文介绍Ubuntu环境下为串口设置别名。 Ubuntu环境下&#xff0c;有时候开发调试会使用到USB转串口&#xff0c;本文介绍在不同使用场景下为串口设置别名的方法。主要分为绑定设备ID和绑定USB端口号。 1.绑定设备ID 绑定设备ID适用于USB转串口的设备ID唯一的情况&#xff0c…

基于安卓android微信小程序的快递取件及上门服务系统

项目介绍 本文从管理员、用户的功能要求出发&#xff0c;快递取件及上门服务中的功能模块主要是实现管理员服务端&#xff1b;首页、个人中心、用户管理、快递下单管理、预约管理、管理员管理、系统管理、订单管理&#xff0c;用户客户端&#xff1b;首页、快递下单、预约管理…