Rust中的Rc. Cell, RefCell

news/2025/1/17 3:48:44/

引用计数Rc

概述: Rc是Rust中用于实现引用计数的类型,它允许多个所有者共享同一个数据。

用法详解:

  • 每当clone一个Rc时,引用计数增加,而每当一个Rc退出作用域时,引用计数减少。
  • 当引用计数变为0时,Rc和它所包裹的数据都会被销毁。
  • Rc的clone不会进行深拷贝,指创建另一个指向包裹值的指针,并增加引用计数

示例:

rust">use std::rc::Rc;fn main() {let rc_examples = "Rc examples".to_string();{println!("rc_a is created");let rc_a: Rc<String> = Rc::new(rc_examples);println!("Reference Count of rc_a: {}", Rc::strong_count(&rc_a)); {println!("rc_a is cloned to rc_b");let rc_b: Rc<String> = Rc::clone(&rc_a);println!("Reference Count of rc_b: {}", Rc::strong_count(&rc_b));println!("Reference Count of rc_a: {}", Rc::strong_count(&rc_a));println!("rc_a and rc_b are equal: {}", rc_a.eq(&rc_b));// 可以直接使用值的方法println!("Length of the value inside rc_a: {}", rc_a.len());// 直接使用值println!("Value of rc_b: {}", rc_b);println!("rc_b is dropped out of scope");}println!("Reference Count of rc_a: {}", Rc::strong_count(&rc_a));println!("rc_a is dropped out of scope");}
}

Cell和RefCell

概述: Rust编译器通过严格的借用规则(多个不可变引用或只有一个可变引用存在)确保程序安全性,但是会降低灵活性。因此提供了Cell和RefCell类型,允许在不可变引用的情况下修改数据。内部是通过unsafe代码实现的

Cell

概述: Cell和RefCell在功能上没有区别,区别在于Cell<T>适用于T实现Copy的情况

示例:

rust">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是&str类型,实现了Copy trait,取到值保存在one变量后,还能同时进行修改,这个违背了Rust的借用规则,但通过Cell就能做到这一点

rust">let c = Cell::new(String::from("asdf"));

这段代码编译器会报错,因为String没有实现Copy trait

RefCell

Rust规则智能指针带来的额外规则
一个数据只有一个所有者Rc/Arc让一个数据可以拥有多个所有者
要么多个不可变借用,要么一个可变借用RefCell实现编译器可变、不可变引用共存
违背规则导致编译错误违背规则导致运行时panic
rust">use std::cell::RefCell;fn main() {let s = RefCell::new(String::from("hello, wolrd"));let s1 = s.borrow();let s2 = s.borrow_mut();println!("{},{}", s1, s2);
}

上面这段代码不会出现编译错误,但是运行时会panic

RefCell为何存在?

从上面看,通过RefCell并不能绕过rust的借用规则,那还有什么用?

对于大型的复杂程序,可以选择使用RefCell来让事情简化。在Rust编译器的ctxt结构体中有大量的RefCell类型的map字段,主要原因是这些map会被分散在各个地方的代码片段所广泛使用或修改,很容易就碰到编译器跑出来的各种错误,但是你不知道如何解决。这时候可以使用RefCell,在运行时发现这些错误,因为一旦有的代码使用不正确,就会panic,我们就知道哪里借用冲突了。

Cell or RefCell

主要区别:

  • Cell只适用于实现了Copy trait类型,用于提供值,而RefCell用于提供引用
  • Cell不会panic,而RefCell会在运行时panic

性能比较:

Cell没有额外的开销,下面两段代码的性能是一样的

rust">// code 1
let x = Cell::new(1);
let y = &x;
let z = &x;
x.set(2);
y.set(3);
z.set(4);
println!("{}", x.get());// code 2
let mut x = 2;
let y = &mut x;
let z = &mut x;
x = 2;
*y = 3;
*z = 4;
println!("{}", x);

但是代码2不能编译成功,因为只能存在一个可变引用

内部可变性

概述: 对一个不可变的值进行可变借用,就是内部可变性

无内部可变性:

rust">fn main() {let x = 5;let y = &mut x;
}

尝试对一个不可变值进行可变借用,破坏了Rust的借用规则

RefCell应用场景:

rust">// 定义在外部库的trait
pub trait Messnger {fn send(&self, msg: String);
}// 我们自己写的代码
struct MsgQueue {msg_cache: Vec<String>,
}impl Messnger for MsgQueue {fn send(&self, msg: String) {self.msg_cache.push(msg);}
}

上面代码会编译错误,因为需要修改self的msg_cache,但是外部库的self是不可变的self,这时候RefCell就派上用场了。

rust">use std::cell::RefCell;pub trait Messnger {fn send(&self, msg: String);
}pub struct MsgQueue {msg_cache: RefCell<Vec<String>>>,
}impl Messnger for MsgQueue {fn send(&self, msg: String) {self.msg_cache.borrow_mut().push(msg);}
}fn main() {let mq = MsgQueue {msg_cache: RefCell::new(Vec::new());};mq.send("hello, world".to_string());
}

Rc+RefCell

概述: 这是一个很常见的组合,前者可以实现一个数据拥有多个所有者,后者可以实现数据的内部可变性

示例:

rust">use std::cell::RefCell;
use std::rc::Rc;fn main() {let s = Rc::new(RefCell::new("Hello, wolrd".to_string()));let s1 = s.clone();let s2 = s.clone();s2.borrow_mut().push_str(", on yeah!");println!("{:?}\n{:?}\n{:?}", s, s1, s2);
}

性能损耗:

非常高,大致相当于没有线程安全版本的C++ std::shared_ptr指针。C++这个指针的主要开销也在于原子性这个并发原语上,毕竟线程安全在哪个语言开销都不小

内存损耗:

二者结合的数据结构与下面类似:

rust">struct Wrapper<T> {// Rcstrong_count: usize,weak_count: usize,// RefCellborrow_count: isize,// 包裹的数据item: T,
}

仅仅多分配了三个usize/isize

CPU损耗:

从CPU来看, 损耗如下:

  • 对Rc<T>解引用是免费的(编译期),但是*带来的间接取值并不免费
  • clone Rc<T>需要将当前的引用计数跟0和usize::Max进行一次比较,然后将计数值加1
  • drop Rc<T>需要将计数值减1,然后跟0进行一次比较
  • 对RefCell进行不可变借用,需要将isize类型的借用计数加1,然后跟0进行比较
  • 对RefCell的不可变借用进行释放,需要将isize减1
  • 对RefCell的可变借用大致跟上面差不多,但需要先跟0比较,然后再减1
  • 对RefCell的可变借用进行释放,需要将isize加1

解决借用冲突

两种方法:

  • Cell::from_mut,将&mut T转换为Cell<T>
  • Cell::as_slice_of_cells,将&Cell<T>转换为&[Cell<T>]

常见的借用冲突问题:

rust">fn is_even(i: i32) -> bool {i % 2 == 0
}fn retain_even(nums: &mut Vec<i32>) {let mut i = 0;for num in nums.iter().filter(|&num| is_even(num)) {nums[i] = *num;i += 1;}nums.truncate(i);
}

会编译错误,因为同时使用了可变借用和不可变借用

可以通过索引来解决这个问题:

rust">fn retain_even(nums: &mut Vec<i32>) {let mut i = 0;for j in 0..nums.len() {if is_even(nums[j]) {nums[i] = nums[j];i += 1;}}nums.truncate(i);
}

但这样不够最佳实践,使用迭代器才是最佳实践

可以使用上面提到的两种方法:

rust">use std::cell::Cell;fn retain_even(nums: &mut Vec<i32>) {let slice: &[Cell<i32>] = Cell::from_mut(&mut nums[..]).as_slice_of_cells();let mut i = 0;for num in slice.iter().filter(|num| is_even(num.get())) {slice[i].set(num.get());i += 1;}nums.truncate(i);
}

http://www.ppmy.cn/news/1563778.html

相关文章

OpenSeaOtter使用手册-安装

OpenSeaOtter是一个易于安装和使用的容器镜像仓库&#xff0c;是现实了docker registry api v2版本。 OpenSeaOtter包含以下步骤: 下载最新二进制版本初始化配置安装linux服务启动服务接入到OpenLinkSaas项目中(可选步骤)创建验证密钥创建镜像分组 下载 你可以从 GitCode - 全球…

Open FPV VTX开源之第一次出图

Open FPV VTX开源之第一次出图 1. 源由2. 连线2.1 飞控2.2 调试 3. serial3.1 启动log - uboot3.2 登录版本 - linux3.3 获取有线IP 4. linux4.1 ssh登录4.2 tfCard 5. PixelPilot出图6. 总结7. 参考资料8. 补充8.1 8812AU网卡8.2 DEBUG串口部分乱码8.3 偶尔启动卡住8.4 花屏、…

深入浅出:React 前端框架解析与应用

引言 随着前端开发技术的不断发展&#xff0c;现代化的前端框架成为了提升开发效率、优化用户体验和构建复杂应用的关键工具。在众多的前端框架中&#xff0c;React凭借其简洁、高效、可扩展的特点&#xff0c;已成为目前最流行的前端框架之一。它由Facebook于2013年发布&…

游戏引擎学习第77天

仓库: https://gitee.com/mrxiao_com/2d_game 回顾昨天的 bug 今天我们继续开发进度&#xff0c;进行调试昨天代码的问题&#xff0c;主要是关于如何跟踪玩家和敌人在世界中的高度位置。虽然我们做的是一款 2D 游戏&#xff0c;但我们希望能够处理多层的房间&#xff0c;玩家…

STM32 FreeRTOS时间片调度---FreeRTOS任务相关API函数---FreeRTOS时间管理

目录 时间片调度简介 FreeRTOS任务相关API函数介绍 延时函数介绍 时间片调度简介 在FreeRTOS中&#xff0c;同等优先级的任务会轮流分享相同的CPU时间&#xff0c;这个时间被称为时间片。在这里&#xff0c;一个时间片的长度等同于SysTick中断的周期。 FreeRTOS任务相关API…

C++实现设计模式---备忘录模式 (Memento)

备忘录模式 (Memento) 备忘录模式 是一种行为型设计模式&#xff0c;它允许在不破坏封装的前提下&#xff0c;捕获和恢复对象的内部状态。通过备忘录模式&#xff0c;可以在程序运行时存储某个对象的历史状态&#xff0c;并在需要时恢复。 意图 提供一种方法&#xff0c;在不…

ansible 检查目录大小

检查目录大小 worker_du.yml# ansible-playbook -i hosts worker_du.yml --limit w10 --- - name: 检查目录大小hosts:- w10 # 可以根据需要修改目标主机# 可以添加更多主机tasks:- name: 获取每台主机 /root/worker01 目录大小shell: du -sh /root/worker01/ | awk {print …

干货答疑分享记录:as导入问题,LSP含义,分屏进入SplashScreen

背景&#xff1a; vip学员群经常会有学员遇到一些常见的android framework开发问题&#xff0c;近期收集整理一些疑问&#xff0c;主要有以下3个&#xff1a; 1、android studio对源码进行导入时候&#xff0c;老是无法跳转到系统source code 2、学员在群里询问dumpOtherPro…