rust 智能指针

智能指针

Box

Box 的使用场景

由于 Box 是简单的封装,除了将值存储在堆上外,并没有其它性能上的损耗。而性能和功能往往是鱼和熊掌,因此 Box 相比其它智能指针,功能较为单一,可以在以下场景中使用它:

  • 特意的将数据分配在堆上
  • 数据较大时,又不想在转移所有权时进行数据拷贝
  • 类型的大小在编译期无法确定,但是我们又需要固定大小的类型时
  • 特征对象,用于说明对象实现了一个特征,而不是某个特定的类型
Box 堆对象分配

因为 Box 允许你将一个值分配到堆上,然后在栈上保留一个智能指针指向堆上的数据。

当栈上数据转移所有权时,实际上是把数据拷贝了一份,最终新旧变量各自拥有不同的数据,因此所有权并未转移。

fn main() {let a = Box::new(3);println!("a = {}", a); // a = 3// 下面一行代码将报错// let b = a + 1; // cannot add `{integer}` to `Box<{integer}>`//正确//let b = *a + 1
}
}
将动态大小类型变为 Sized 固定大小类型

Rust 需要在编译时知道类型占用多少空间,如果一种类型在编译时无法知道具体的大小,那么被称为动态大小类型 DST。

其中一种无法在编译时知道大小的类型是递归类型:在类型定义中又使用到了自身,或者说该类型的值的一部分可以是相同类型的其它值,这种值的嵌套理论上可以无限进行下去,所以 Rust 不知道递归类型需要多少空间:

enum List {Cons(i32, List),Nil,
}

以上就是函数式语言中常见的 Cons List,它的每个节点包含一个 i32 值,还包含了一个新的 List,因此这种嵌套可以无限进行下去,Rust 认为该类型是一个 DST 类型,并给予报错:

error[E0072]: recursive type `List` has infinite size //递归类型 `List` 拥有无限长的大小--> src/main.rs:3:1|
3 | enum List {| ^^^^^^^^^ recursive type has infinite size
4 |     Cons(i32, List),|               ---- recursive without indirection

此时若想解决这个问题,就可以使用我们的 Box:

enum List {Cons(i32, Box<List>),Nil,
}

只需要将 List 存储到堆上,然后使用一个智能指针指向它,即可完成从 DST 到 Sized 类型(固定大小类型)的华丽转变。

Box 中还提供了一个非常有用的关联函数:Box::leak,它可以消费掉 Box 并且强制目标值从内存中泄漏,读者可能会觉得,这有啥用啊?

fn main() {let s = gen_static_str();println!("{}", s);
}fn gen_static_str() -> &'static str{let mut s = String::new();s.push_str("hello, world");Box::leak(s.into_boxed_str())
}
特征对象

在 Rust 中,想实现不同类型组成的数组只有两个办法:枚举和特征对象,前者限制较多,因此后者往往是最常用的解决办法。


trait Draw {fn draw(&self);
}struct Button {id: u32,
}
impl Draw for Button {fn draw(&self) {println!("这是屏幕上第{}号按钮", self.id)}
}struct Select {id: u32,
}impl Draw for Select {fn draw(&self) {println!("这个选择框贼难用{}", self.id)}
}fn main() {let elems: Vec<Box<dyn Draw>> = vec![Box::new(Button { id: 1 }), Box::new(Select { id: 2 })];for e in elems {e.draw()}
}
Box::leak

Box 中还提供了一个非常有用的关联函数:Box::leak,它可以消费掉 Box 并且强制目标值从内存中泄漏,读者可能会觉得,这有啥用啊?

其实还真有点用,例如,你可以把一个 String 类型,变成一个 'static 生命周期的 &str 类型:

fn main() {let s = gen_static_str();println!("{}", s);
}fn gen_static_str() -> &'static str{let mut s = String::new();s.push_str("hello, world");Box::leak(s.into_boxed_str())
}

Deref 解引用

智能指针的名称来源,主要就在于它实现了 Deref 和 Drop 特征,这两个特征可以智能地帮助我们节省使用上的负担:

Deref 可以让智能指针像引用那样工作,这样你就可以写出同时支持智能指针和引用的代码,例如 *T
Drop 允许你指定智能指针超出作用域后自动执行的代码

常规引用的解引用。

常规引用是一个指针类型,包含了目标数据存储的内存地址。对常规引用使用 * 操作符,就可以通过解引用的方式获取到内存地址对应的数据值:

fn main() {let x = 5;let y = &x;assert_eq!(5, x);assert_eq!(5, *y);
}

这里 y 就是一个常规引用,包含了值 5 所在的内存地址,然后通过解引用 *y,我们获取到了值 5。如果你试图执行 assert_eq!(5, y);,代码就会无情报错,因为你无法将一个引用与一个数值做比较:

error[E0277]: can't compare `{integer}` with `&{integer}` //无法将{integer} 与&{integer}进行比较--> src/main.rs:6:5|
6 |     assert_eq!(5, y);|     ^^^^^^^^^^^^^^^^^ no implementation for `{integer} == &{integer}`|= help: the trait `PartialEq<&{integer}>` is not implemented for `{integer}`// 你需要为{integer}实现用于比较的特征PartialEq<&{integer}>
智能指针解引用

实现 Deref 后的智能指针结构体,就可以像普通引用一样,通过 * 进行解引用,例如 Box 智能指针:

fn main() {let x = Box::new(1);let sum = *x + 1;
}

智能指针 x 被 * 解引用为 i32 类型的值 1,然后再进行求和。

定义自己的智能指针

由于 Box 本身很简单,并没有包含类如长度、最大长度等信息,因此用一个元组结构体即可。

struct MyBox<T>(T);impl<T> MyBox<T> {fn new(x: T) -> MyBox<T> {MyBox(x)}
}

跟 Box 一样,我们的智能指针也持有一个 T 类型的值,然后使用关联函数 MyBox::new 来创建智能指针。由于还未实现 Deref 特征,此时使用 * 肯定会报错:

fn main() {let y = MyBox::new(5);assert_eq!(5, *y);
}

运行后,报错如下:

error[E0614]: type `MyBox<{integer}>` cannot be dereferenced--> src/main.rs:12:19|
12 |     assert_eq!(5, *y);|                   ^^
为智能指针实现 Deref 特征

现在来为 MyBox 实现 Deref 特征,以支持 * 解引用操作符:

use std::ops::Deref;impl<T> Deref for MyBox<T> {type Target = T;fn deref(&self) -> &Self::Target {&self.0}
}

很简单,当解引用 MyBox 智能指针时,返回元组结构体中的元素 &self.0,有几点要注意的:

在 Deref 特征中声明了关联类型 Target,在之前章节中介绍过,关联类型主要是为了提升代码可读性
deref 返回的是一个常规引用,可以被 * 进行解引用

* 背后的原理

当我们对智能指针 Box 进行解引用时,实际上 Rust 为我们调用了以下方法:

*(y.deref())

首先调用 deref 方法返回值的常规引用,然后通过 * 对常规引用进行解引用,最终获取到目标值。

要注意的是,* 不会无限递归替换,从 *y 到 *(y.deref()) 只会发生一次。

函数和方法中的隐式 Deref 转换

对于函数和方法的传参,Rust 提供了一个极其有用的隐式转换:Deref 转换。若一个类型实现了 Deref 特征,那它的引用在传给函数或方法时,会根据参数签名来决定是否进行隐式的 Deref 转换,例如:

fn main() {let s = String::from("hello world");display(&s)
}fn display(s: &str) {println!("{}",s);
}

以上代码有几点值得注意:

String 实现了 Deref 特征,可以在需要时自动被转换为 &str 类型

  • &s 是一个 &String 类型,当它被传给 display 函数时,自动通过 Deref 转换成了 &str
  • 必须使用 &s 的方式来触发 Deref(仅引用类型的实参才会触发自动解引用)
连续的隐式 Deref 转换

Deref 可以支持连续的隐式转换,直到找到适合的形式为止:

fn main() {let s = MyBox::new(String::from("hello world"));display(&s)
}fn display(s: &str) {println!("{}",s);
}

这里我们使用了之前自定义的智能指针 MyBox,并将其通过连续的隐式转换变成 &str 类型:首先 MyBox 被 Deref 成 String 类型,结果并不能满足 display 函数参数的要求,编译器发现 String 还可以继续 Deref 成 &str,最终成功的匹配了函数参数。

引用归一化

Rust 编译器实际上只能对 &v 形式的引用进行解引用操作,那么问题来了,如果是一个智能指针或者 &&&&v 类型的呢? 该如何对这两个进行解引用?

答案是:Rust 会在解引用时自动把智能指针和 &&&&v 做引用归一化操作,转换成 &v 形式,最终再对 &v 进行解引用:

把智能指针(比如在库中定义的,Box、Rc、Arc、Cow 等)从结构体脱壳为内部的引用类型,也就是转成结构体内部的 &v
把多重&,例如 &&&&&&&v,归一成 &v

三种 Deref 转换

实际上 Rust 还支持将一个可变的引用转换成另一个可变的引用以及将一个可变引用转换成不可变的引用,规则如下:

  • 当 T: Deref<Target=U>,可以将 &T 转换成 &U,也就是我们之前看到的例子
  • 当 T: DerefMut<Target=U>,可以将 &mut T 转换成 &mut U
  • 当 T: Deref<Target=U>,可以将 &mut T 转换成 &U
    来看一个关于 DerefMut 的例子:
struct MyBox<T> {v: T,
}impl<T> MyBox<T> {fn new(x: T) -> MyBox<T> {MyBox { v: x }}
}use std::ops::Deref;impl<T> Deref for MyBox<T> {type Target = T;fn deref(&self) -> &Self::Target {&self.v}
}use std::ops::DerefMut;impl<T> DerefMut for MyBox<T> {fn deref_mut(&mut self) -> &mut Self::Target {&mut self.v}
}fn main() {let mut s = MyBox::new(String::from("hello, "));display(&mut s)
}fn display(s: &mut String) {s.push_str("world");println!("{}", s);
}

以上代码有几点值得注意:

Rc 与 Arc

引用计数(reference counting),顾名思义,通过记录一个数据被引用的次数来确定该数据是否正在被使用。当引用次数归零时,就代表该数据不再被使用,因此可以被清理释放。

use std::rc::Rc;
fn main() {    
let a = Rc::new(String::from("hello, world"));    
let b = Rc::clone(&a);    
assert_eq!(2, Rc::strong_count(&a));    
assert_eq!(Rc::strong_count(&a), Rc::strong_count(&b))}

以上代码我们使用 Rc::new 创建了一个新的 Rc 智能指针并赋给变量 a,该指针指向底层的字符串数据。智能指针 Rc 在创建时,还会将引用计数加 1,此时获取引用计数的关联函数 Rc::strong_count 返回的值将是 1。Rc::clone接着,我们又使用 Rc::clone 克隆了一份智能指针 Rc,并将该智能指针的引用计数增加到 2。由于 a 和 b 是同一个智能指针的两个副本,因此通过它们两个获取引用计数的结果都是 2。不要被 clone 字样所迷惑,以为所有的 clone 都是深拷贝。这里的 clone 仅仅复制了智能指针并增加了引用计数,并没有克隆底层数据,因此 a 和 b 是共享了底层的字符串 s,这种复制效率是非常高的。

Rc 简单总结
  • Rc/Arc 是不可变引用,你无法修改它指向的值,只能进行读取,如果要修改,需要配合内部可变性 RefCell 或互斥锁 Mutex
  • 一旦最后一个拥有者消失,则资源会自动被回收,这个生命周期是在编译期就确定下来的
  • Rc 只能用于同一线程内部,想要用于线程之间的对象共享,你需要使用 Arc
  • Rc 是一个智能指针,实现了 Deref 特征,因此你无需先解开 Rc 指针,再使用里面的 T,而是可以直接使用 T
Arc

Arc 是 Atomic Rc 的缩写,顾名思义:原子化的 Rc 智能指针。它能保证我们的数据能够安全的在线程间共享即可.
Arc 和 Rc 拥有完全一样的 API,修改起来很简单:

use std::sync::Arc;use std::thread;
fn main() {    
let s = Arc::new(String::from("多线程漫游者"));    
for _ in 0..10 {        
let s = Arc::clone(&s);        
let handle = thread::spawn(move || {           println!("{}", s)       });    }}

Cell 和 RefCell

因此 Rust 提供了 Cell 和 RefCell 用于内部可变性,简而言之,可以在拥有不可变引用的同时修改目标数据。

Cell

Cell 和 RefCell 在功能上没有区别,区别在于 Cell 适用于 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);
}
RefCell

由于 Cell 类型针对的是实现了 Copy 特征的值类型,因此在实际开发中,Cell 使用的并不多,因为我们要解决的往往是可变、不可变引用共存导致的问题,此时就需要借助于 RefCell 来达成目的。

所有权、借用规则与这些智能指针做一个对比:

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

Rust 中,一个常见的组合就是 Rc 和 RefCell 在一起使用,前者可以实现一个数据拥有多个所有者,后者可以实现数据的可变性:


use std::cell::RefCell;
use std::rc::Rc;
fn main() {let s = Rc::new(RefCell::new("我很善变,还拥有多个主人".to_string()));let s1 = s.clone();let s2 = s.clone();// let mut s2 = s.borrow_mut();s2.borrow_mut().push_str(", oh yeah!");println!("{:?}\n{:?}\n{:?}", s, s1, s2);
}

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

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

相关文章

Flink之DataStream API开发Flink程序过程与Flink常见数据类型

开发Flink程序过程与Flink常见数据类型 DataStream APIFlink三层APIDataStream API概述 开发Flink程序过程添加依赖创建执行环境执行模式创建Data Source应用转换算子创建Data Sink触发程序执行示例 Flink常见数据类型基本数据类型字符串类型时间和日期类型数组类型元组类型列表…

【通信系列 6 -- AT 命令介绍】

文章目录 1. 背景介绍1.2 AT的命令格式1.3 AT指令用法1.3.1 指令执行结果 1.2 CP 常用AT指令1.2.1 CP 模式设置1.2.2 网络相关1.2.3 IP获取1.2.4 Band 设置1.2.5 电话相关1.2.6 SIM卡检测1.2.7 cmwap 和cmnet1.2.8 AT 写 IMEI 1. 背景介绍 AT 命令一般分为三种&#xff1a; C…

1.1 向量与线性组合

一、向量的基础知识 两个独立的数字 v 1 v_1 v1​ 和 v 2 v_2 v2​&#xff0c;将它们配对可以产生一个二维向量 v \boldsymbol{v} v&#xff1a; 列向量 v v [ v 1 v 2 ] v 1 v 的第一个分量 v 2 v 的第二个分量 \textbf{列向量}\,\boldsymbol v\kern 10pt\boldsymbol …

【Java】如何将File转换成MultipartFile

假如你把你的后端项目部署在服务器上的时候&#xff0c;然后你要去读取某个路径下的文件&#xff0c;然后你就要提供文件的路径&#xff0c;然后获取到该文件对象&#xff0c;但是你需要将当前文件对象File转换成MultipartFile再发送http请求传递到其他服务器上&#xff0c;这样…

GPIO子系统(三)

1&#xff0c;简述 GPIO 资源是相对来说较为简单&#xff0c;而且比较通用&#xff08;比如 LED 灯&#xff09;&#xff0c;而 Linux 的 GPIO 驱动属于 Linux Driver 中较为容易上手的部分&#xff0c;但是简单归简单&#xff0c;在 Linux 系统中&#xff0c;要使用 GPIO 资源…

mysql.mongoDb,neo4j数据库对比

Mysql与MongoDb和Neo4j的一些对比 主要区别 MySQL&#xff1a; 1.MySQL是一种关系型数据库管理系统&#xff08;RDBMS&#xff09;&#xff0c;广泛用于处理结构化数据。 2.它支持SQL语言&#xff0c;具备成熟的事务处理和数据一致性能力。 3.MySQL适用于大多数传统的基于表格…

UE4和C++ 开发-头文件(.h) 和实现文件(.cpp)区别

.h文件和.cpp文件是C程序中的两种不同类型的文件。 .h文件通常包含类、函数和变量的声明&#xff0c; 而.cpp文件包含这些声明的实现。 .h文件中的声明通常是公共的&#xff0c;可以被其他文件包含和使用。.cpp文件中的实现通常是私有的&#xff0c;只能在该文件中使用。 在…

高级网络调试技巧:使用Charles Proxy捕获和修改HTTP/HTTPS请求

今天我将与大家分享一种强大的网络调试技巧&#xff0c;那就是使用Charles Proxy来捕获和修改HTTP/HTTPS请求。如果您是一位开发人员或者网络调试爱好者&#xff0c;那么这个工具肯定对您有着很大的帮助。接下来&#xff0c;让我们一起来学习如何使用Charles Proxy进行高级网络…

区块链加密虚拟货币交易平台安全解决方案

区块链机密货币交易锁遭入侵&#xff0c;安全存在隐患。使用泰雷兹Protect server HSM加密机&#xff0c;多方位保护您的数据&#xff0c;并通过集中化管理&#xff0c;安全的存储密钥。 引文部分&#xff1a; 损失7000万美元!黑客入侵香港区块链加密货币交易所 2023年9月&…

计算机毕业设计选什么题目好?springboot 健身房管理系统

✍✍计算机编程指导师 ⭐⭐个人介绍&#xff1a;自己非常喜欢研究技术问题&#xff01;专业做Java、Python、微信小程序、安卓、大数据、爬虫、Golang、大屏等实战项目。 ⛽⛽实战项目&#xff1a;有源码或者技术上的问题欢迎在评论区一起讨论交流&#xff01; ⚡⚡ Java实战 |…

HTML5简介-HTML5 新增语义化标签-HTML5 新增多媒体标签

一、HTML5简介 HTML5&#xff0c;全称为HyperText Markup Language 5&#xff0c;是HTML的第五个版本&#xff0c;由万维网联盟&#xff08;World Wide Web Consortium&#xff0c;W3C&#xff09;和Web Hypertext Application Technology Working Group&#xff08;WHATWG&am…

github创建个人网页登录后404无法显示的问题

1.首先必须要有内容&#xff0c;默认是会找index.html文件&#xff0c;找不到该文件会找readme.md文件&#xff0c;也就是说最简单的方法是&#xff0c;创建了与用户名同名的repository后username.github.io后&#xff0c;添加一个readme.md文件&#xff0c;得在readme里打点字…

十四、【图章工具组】

文章目录 仿制图章图案图章 仿制图章 纺织图和章工具跟我们之前所用到的修补工具类似,需要我们先按住Alt键选住一块区域&#xff0c;然后调整它的硬度在用我们选择的区域去覆盖&#xff0c;需要注意的是&#xff0c;我们去做的时候尽量一笔覆盖我们想要遮住的区域: 图案图章…

Blender:对模型着色

Blender&#xff1a;使用立方体制作动漫头像-CSDN博客 上一步已经做了一个头像模型&#xff0c;我做的太丑了&#xff0c;就以这个外星人头像为例 首先切换到着色器编辑器 依次搜索&#xff1a;纹理坐标、映射、分离xyz和颜色渐变 这里的功能也是非常丰富和强大&#xff0c…

【微服务部署】九、使用Docker Compose搭建高可用双机热备MySQL数据库

通常&#xff0c;一般业务我们使用云服务器提供的数据库&#xff0c;无论是MySQL数据库还是其他数据库&#xff0c;云服务厂商都提供了主备功能&#xff0c;我们不需要自己配置处理。而如果需要我们自己搭建数据库&#xff0c;那么考虑到数据的高可用性、故障恢复和扩展性&…

RFID拓展的相关问答

基于&#xff1a; Research Reading: Smart Parking Applications Using RFID Technology-CSDN博客这篇文章总结了无线射频识别&#xff08;RFID&#xff09;技术在自动化中的应用及其在停车场管理系统中的解决方案。文章提到&#xff0c;RFID技术在自动化中可以降低交易成本&…

【数据结构C/C++】双向链表的增删改查

文章目录 CC408考研各数据结构C/C代码&#xff08;Continually updating&#xff09; 对我个人而言&#xff0c;在开发过程中使用的比较多的就是双向链表了。 很多重要的代码优化都会使用到基于双向链表实现的数据机构。 比如我们常用的HashMap&#xff0c;我们知道Key其实是无…

Macos音乐制作:Ableton Live 11 Suite for Mac中文版

Ableton Live 11是一款数字音频工作站软件&#xff0c;用于音乐制作、录音、混音和现场演出。它由Ableton公司开发&#xff0c;是一款极其流行的音乐制作软件之一。 以下是Ableton Live 11的一些主要特点和功能&#xff1a; Comping功能&#xff1a;Live 11增加了Comping功能…

css设置文本溢出隐藏...

在CSS中&#xff0c;文本溢出可以使用text-overflow属性来处理&#xff0c;下面分别介绍单行文本溢出和多行文本溢出的处理方法1&#xff1a; 单行文本溢出。需要使用text-overflow: ellipsis;来显示省略号。需要注意的是&#xff0c;为了兼容部分浏览器&#xff0c;还需要设置…

深度思考线程池面经之四:使用线程池的各种特性

8 线程池&#xff08;百度acg百度云一面&#xff09; 8.1 你是用到哪个线程池呢&#xff0c;在哪个场景中使用呢 答&#xff1a;在秒杀系统中使用了newCachedThreadPool这个线程池 8.2 自定义线程池的参数&#xff0c;你是怎么设置的呢 8.3 写一个自定义线程池&#xff0c;…