【跟小嘉学 Rust 编程】十五、智能指针

系列文章目录

【跟小嘉学 Rust 编程】一、Rust 编程基础
【跟小嘉学 Rust 编程】二、Rust 包管理工具使用
【跟小嘉学 Rust 编程】三、Rust 的基本程序概念
【跟小嘉学 Rust 编程】四、理解 Rust 的所有权概念
【跟小嘉学 Rust 编程】五、使用结构体关联结构化数据
【跟小嘉学 Rust 编程】六、枚举和模式匹配
【跟小嘉学 Rust 编程】七、使用包(Packages)、单元包(Crates)和模块(Module)来管理项目
【跟小嘉学 Rust 编程】八、常见的集合
【跟小嘉学 Rust 编程】九、错误处理(Error Handling)
【跟小嘉学 Rust 编程】十一、编写自动化测试
【跟小嘉学 Rust 编程】十二、构建一个命令行程序
【跟小嘉学 Rust 编程】十三、函数式语言特性:迭代器和闭包
【跟小嘉学 Rust 编程】十四、关于 Cargo 和 Crates.io
【跟小嘉学 Rust 编程】十五、智能指针

文章目录

  • 系列文章目录
    • @[TOC](文章目录)
  • 前言
  • 一、智能指针
    • 1.1、智能指针(smart point)
    • 1.2、Box 堆内存分配
      • 1.2.1、场景1:堆内存上分配数据
      • 1.2.2、场景2: cons list
    • 1.3、Deref 解引用
      • 1.3.1、Deref trait
      • 1.3.2、三种 Deref 转换
    • 1.4、Drop 释放资源
      • 1.4.1、Drop trait
      • 1.4.2、使用 std::mem::drop 来提前 drop
    • 1.5、引用计数智能指针(`RC<T>` 和 `Arc<T>`)
      • 1.5.1、`RC<T>`
      • 1.5.1、原子引用计数(Atomic reference counter)
    • 1.6、Cell 与 RefCell 内部可变性
      • 1.6.1、内部可变性(interior mutability)
      • 1.6.2、Cell<T>
      • 1.6.3、RefCell<T>
      • 1.6.4、Cell 和 RefCell
      • 1.6.5、解决借用冲突
    • 1.7、Weak 和引用循环
      • 1.7.1、引用循环和内存泄漏
      • 1.7.2、Weak
      • 1.7.3、unsafe
  • 总结

前言

指针是一个包含了内存地址的变量,该内存地址引用或执行了另外的数据。在Rust中最常见的指针类型就是引用。不同的是在Rust中引用被赋予更深的含义就是借用其他变量的值。

主要教材参考 《The Rust Programming Language》


一、智能指针

1.1、智能指针(smart point)

智能指针是一个复杂的数据结构,包含了比引用更多的信息,例如元数据,当前长度,最大可用长度等。

在之前章节实际上我们已经见识过多种智能指针了,例如动态字符串 String 和动态数据 Vec。

智能指针往往是基于结构体实现,它与我们自定义的结构体最大的区别在于它实现了 Deref 和 Drop 特征:

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

1.2、Box 堆内存分配

在Rust 中,所有值默认都是在栈内存上分配,通过创建 Box<T> 可用把值装箱,使它在堆上分配。Box<T> 是一个智能指针,因为它实现了 Deref trait,它允许Box<T> 值被当作引用对待,当 Box<T> 值离开作用域时,由于它实现了 Drop trait ,首先删除其指向堆堆数据,然后删除自身。

使用场景

  • 在编译时,某类型的大小无法确定,但使用该类型时,上下文却需要知道它确切的大小;
  • 当你有大量数据,想移交所有权,但需要确保在操作时数据不会被复制;
  • 使用某个值,你只关心它是否实现了特定的 trait ,而不关心它的具体类型;

1.2.1、场景1:堆内存上分配数据


fn main() {let a = Box::new(1);  // Immutableprintln!("{}", a);    // Output: 1let mut b = Box::new(1);  // Mutable*b += 1;println!("{}", b);    // Output: 2
}

Box 的主要特性是单一所有权,即同时智能有一个人拥有对其指向数据的所有权,并且同时智能存在一个可变引用或多个不可变引用,这一点与Rust中其他属于堆上的数据行为一致。

1.2.2、场景2: cons list

cons list 是来自 Lisp 语言的一种数据结构。cons list 里面每个成员都包含两个元素:当前项都值和下一个元素。cons list 里的最后一个成员只包含一个 nil 值,没有下一个元素。

Box<T> 是一个指针,Rust知道它需要多少空间,因为指针的大小不会基于它指向的数据的大小变化而变化。

use crate::List::{Cons, Nil};fn main() {let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3,Box::new(Nil))))));
}enum List {Cons(i32, Box<List>),Nil,
}

1.3、Deref 解引用

1.3.1、Deref trait

Deref Trait 允许我们重载解引用运算符 *。实现 Deref 的智能指针可以被当作引用来对待,也就是说可以对智能指针使用 *运算符来解引用。

#[stable(feature = "rust1", since = "1.0.0")]
impl<T: ?Sized> Deref for Box<T> {type Target = T;fn deref(&self) -> &T {&**self}
}

1.3.2、三种 Deref 转换

在之前,我们讲的都是不可变的 Deref 转换,实际上 Rust 还支持将一个可变的引用转换成另一个可变的引用以及将一个可变引用转换成不可变的引用,规则如下:

当 T: Deref<Target=U>,可以将 &T 转换成 &U,也就是我们之前看到的例子
当 T: DerefMut<Target=U>,可以将 &mut T 转换成 &mut U
当 T: Deref<Target=U>,可以将 &mut T 转换成 &U

1.4、Drop 释放资源

1.4.1、Drop trait

Drop trait 主要作用是释放实现者实例拥有的资源,它只有一个方法 drop。当实例离开作用域时会自动调用该方法,从而调用实现者指定的代码。

#[stable(feature = "rust1", since = "1.0.0")]
unsafe impl<#[may_dangle] T: ?Sized> Drop for Box<T> {fn drop(&mut self) {// FIXME: Do nothing, drop is currently performed by compiler.}
}

1.4.2、使用 std::mem::drop 来提前 drop

Rust 不允许手动调用 Drop trait 的 drop 方法,但是可以 使用标准库的 std::mem::drop 来提前 drop。

1.5、引用计数智能指针(RC<T>Arc<T>)

1.5.1、RC<T>

RC<T> 主要用于同一个堆上所有分配的数据区域需要多个只读访问的情况,比起使用比起使用 Box<T> 然后创建多个不可变引用的方法更优雅也更直观一些,以及比起单一所有权,Rc<T> 支持多所有权。

Rc 为 Reference Counter 的缩写,即为引用计数,Rust 的 Runtime 会实时记录一个 Rc<T> 当前被引用的次数,并在引用计数归零时对数据进行释放(类似 Python 的 GC 机制)。因为需要维护一个记录 Rc<T> 类型被引用的次数,所以这个实现需要 Runtime Cost。

use std::rc::Rc;fn main() {let a = Rc::new(1);println!("count after creating a = {}", Rc::strong_count(&a));let b = Rc::clone(&a);println!("count after creating b = {}", Rc::strong_count(&a));{let c = Rc::clone(&a);println!("count after creating c = {}", Rc::strong_count(&a));}println!("count after c goes out of scope = {}", Rc::strong_count(&a));
}

需要注意

  • RC<T> 是完全不可变,可以理解为同一个内存上的数据同时存在多个只读指针
  • RC<T> 只适用单线程,尽管从概念上讲不同线程间只读指针是完全安全的,但是由于 RC<T> 没有实现多个线程间保证计数一致性,如果你尝试多线程内使用,会报错;

1.5.1、原子引用计数(Atomic reference counter)

此时引用计数就可以在不同线程中安全的被使用了。

use std::thread;
use std::sync::Arc;fn main() {let a = Arc::new(1);thread::spawn(move || {let b = Arc::clone(&a);println!("{}", b);  // Output: 1}).join();
}

1.6、Cell 与 RefCell 内部可变性

1.6.1、内部可变性(interior mutability)

内部可变性(interior mutability) 是 Rust 的设计模式之一,它允许你在只持有不可变引用的前提下对数据进行修改,数据结构中使用了 unsafe 代码来绕过 Rust 正常的可变性和借用规则。

1.6.2、Cell

Cell 和 Refcell 在功能上没有区别,区别在于 Cell 适用于 T 实现 Copy 的情况

1.6.3、RefCell

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

Rust 规则智能指针带来的额外规则
一个数据只有一个所有者Rc/Arc 让一个数据可以拥有多个所有者
要么多个不可变借用,要么一个可变借用RefCell 实现编译器可变、不可变引用共存
违背规则导致编译错误违背规则导致运行时 panic

可以看出,Rc/Arc 和 RefCell 合在一起,解决了 Rust 中严苛的所有权和借用规则带来的某些场景下难使用的问题。但是它们并不是银弹,例如 RefCell 实际上并没有解决可变引用和引用可以共存的问题,只是将报错从编译期推迟到运行时,从编译器错误变成了 panic 异常:

1.6.4、Cell 和 RefCell

  • Cell 只适用于 Copy 类型,用于提供值,而RefCell 用于提供引用
  • Cell 不会panic ,而 RefCell 会
  • Cell 没有额外的性能损耗

从 CPU 来看,损耗如下:

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

1.6.5、解决借用冲突

在 Rust 1.37 版本中新增了两个非常实用的方法:

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

1.7、Weak 和引用循环

1.7.1、引用循环和内存泄漏

Rust 的内存安全机制可以保证很难发生内存泄漏。但是不代表不会内存泄漏。一个典型的例子就是同时使用 Rc 和 RefCell 创建循环引用,最终这些引用的计数都无法被归零,因此 Rc 拥有的值也不会被释放清理。

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的初始化rc计数 = {}", Rc::strong_count(&a));println!("a指向的节点 = {:?}", a.tail());// 创建`b`到`a`的引用let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));println!("在b创建后,a的rc计数 = {}", Rc::strong_count(&a));println!("b的初始化rc计数 = {}", Rc::strong_count(&b));println!("b指向的节点 = {:?}", b.tail());// 利用RefCell的可变性,创建了`a`到`b`的引用if let Some(link) = a.tail() {*link.borrow_mut() = Rc::clone(&b);}println!("在更改a后,b的rc计数 = {}", Rc::strong_count(&b));println!("在更改a后,a的rc计数 = {}", Rc::strong_count(&a));// 下面一行println!将导致循环引用// 我们可怜的8MB大小的main线程栈空间将被它冲垮,最终造成栈溢出// println!("a next item = {:?}", a.tail());
}

如何防止循环引用

  • 开发者去注意细节
  • 使用 Weak

1.7.2、Weak

Weak 类似 RC 但是和 RC持有所有权不同,Weak 不必持有所有权,仅仅保存一份指向数据的弱引用,如果你要想访问数据,需要通过 Weak 指针的 upgrade 方法实现,该方法返回个类型为 Option<Rc<T>> 的值。

所谓弱引用就是不保证引用关系存在,如果不存在,就返回None。

因为 Weak 引用不计入所有权,因此它无法阻止所引用的内存值被释放掉,而且 Weak 本身不对值的存在性做任何担保,引用的值还存在就返回 Some,不存在就返回 None。

WeakRC
不计数计数
不拥有所有权拥有值的所有权
不阻止值被释放(drop)所有权计数归零,才能drop
引用存在返回some,不存在返回None引用值必定存在
通过 upgrade 取到Option<Rc<T>> 再取值通过 Deref 自动解引用,取值无需任何操作

弱引用非常适合如下场景

  • 持有一个 Rc 对象的临时引用,并且不在乎引用的值是否依然存在
  • 阻止 Rc 导致的循环引用,因为 Rc 的所有权机制,会导致多个 Rc 都无法计数归零

1.7.3、unsafe

除了使用 Rust 标准库提供的这些类型,你还可以使用 unsafe 里的裸指针来解决这些棘手的问题,但是由于我们还没有讲解 unsafe。

虽然 unsafe 不安全,但是在各种库的代码中依然很常见用它来实现自引用结构,主要优点如下:

  • 性能高,毕竟直接用裸指针操作
  • 代码更简单更符合直觉: 对比下 Option<Rc<RefCell<Node>>>

总结

以上就是今天要讲的内容

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

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

相关文章

E - Excellent Views

Problem - E - Codeforces 问题描述&#xff1a;数组H大小都不相同。从i到j是可行的&#xff0c;当且仅当 不存在 k &#xff0c;使 ∣ i − k ∣ ≤ ∣ i − j ∣ , H k > H j 不存在k&#xff0c;使 \\ |i - k| \leq |i - j|, \quad H_k > H_j 不存在k&#xff0c;使…

华为云渲染实践

// 编者按&#xff1a;云计算与网络基础设施发展为云端渲染提供了更好的发展机会&#xff0c;华为云随之长期在自研图形渲染引擎、工业领域渲染和AI加速渲染三大方向进行云渲染方面的探索与研究。本次LiveVideoStackCon 2023上海站邀请了来自华为云的陈普&#xff0c;为大家分…

【sql】MongoDB 查询 高级用法

【sql】MongoDB 查询 高级用法 一、基本查询指定字段 db.getCollection(students).find({}, {name: 1, score: 1}) 二、指定字段别名 db.getCollection(students).find({}, {"name":1, "score":1, "grade":"$grade.grade"}) 这里将…

【ag-grid-vue】column

网格中的每一列都使用列定义(ColDef)来定义。列根据在网格选项中指定的列定义的顺序在网格中定位。 列定义 下面的例子展示了一个定义了3列的简单网格: <template><ag-grid-vuestyle"height: 300px; width: 1000px"class"ag-theme-balham":colum…

集群路由策略

路由策略 1.第一个&#xff1a;当选择该策略时&#xff0c;会选择执行器注册地址的第一台机器执行&#xff0c;如果第一台机器出现故障&#xff0c;则调度任务失败。 2.第二个&#xff1a;当选择该策略时&#xff0c;会选择执行器注册地址的第二台机器执行&#xff0c;如果第二…

自动化的驱动力,工控机助您实现智能生产!

“智能工厂建设如火如荼&#xff0c;部分成果已经落地&#xff0c;在大规模资金投入的市场催化下&#xff0c;海尔、海信等制造企业通过智能工厂手段推进生产效率成倍增长的新闻层出不穷。在工业4.0时代&#xff0c;“中国制造2025”战略中&#xff0c;智能工厂构建都是其中不可…

【KMP算法-代码随想录】

目录 1.什么是KMP2.什么是next数组3.什么是前缀表&#xff08;1&#xff09;前后缀含义&#xff08;2&#xff09;最长公共前后缀&#xff08;3&#xff09;前缀表的必要性 4.计算前缀表5.前缀表与next数组&#xff08;1&#xff09;使用next数组来匹配 6.构造next数组&#xf…

1.linux的常用命令

目录 一、Linux入门 二、Linux文件系统目录 三、Linux的vi和vim的使用 四、Linux的关机、重启、注销 四、Linux的用户管理 五、Linux的运行级别 六、Linux的文件目录指令 七、Linux的时间日期指令 八、Linux的压缩和解压类指令 九、Linux的搜索查找指令 ​​​​​​…

windows可视化界面管理服务器上的env文件

需求&#xff1a;在 Windows 环境中通过可视化界面编辑位于 Linux 主机上的 env 文件的情况&#xff0c;我现在环境是windows环境&#xff0c;我的env文件在linux的192.168.20.124上&#xff0c;用户是op&#xff0c;密码是op&#xff0c;文件绝对路径是/home/op/compose/env …

无涯教程-PHP - 性能优化

根据Zend小组的说明,以下插图显示了PHP 7与PHP 5.6和基于流行的基于PHP的应用程序上的HHVM 3.7。 Magento 1.9 与执行Magento事务的PHP 5.6相比&#xff0c;PHP 7的运行速度证明是其两倍。 Drupal 7 在执行Drupal事务时&#xff0c;与PHP 5.6相比&#xff0c;PHP 7的运行速度…

SQL 大小敏感问题

在SQL中&#xff0c;关键字和函数名 是不区分 大小写的 比如&#xff08;select、where、order by 、group by update 等关键字&#xff09;&#xff0c;以及函数(ABS、MOD、round、min等) window系统默认是大小写不敏感 &#xff08;ZEN文件和zen 文件 不能同时存在&#xff…

fckeditor编辑器的两种使用方法

需要的资源包我放我资源里了&#xff0c;不要积分 https://download.csdn.net/download/wybshyy/88245895 首先把FredCK.FCKeditorV2.dll添加到引用 具体方法如下&#xff0c;一个是客户端版本&#xff0c;一个是服务器端版本 客户端版本&#xff1a; <% Page Language…

【vue3+ts项目】配置husky+配置commitlint

上一篇文章中配置了eslint校验代码工具 【vue3ts项目】配置eslint校验代码工具&#xff0c;eslintprettierstylelint 1、配置husky 每次手动执行命令才能格式化代码&#xff0c;如果有人没有格式化就提交到远程仓库&#xff0c;这个规范就起不到作用了&#xff0c;所有需要强…

Arcgis colorRmap

arcgis中colorRmap对应的名称&#xff1a; 信息来源&#xff1a;https://developers.arcgis.com/documentation/common-data-types/raster-function-objects.htm 在arcpy中使用方法&#xff1a; import arcpy cr arcpy.mp.ColorRamp("Yellow to Red")python中 ma…

wqs二分

前提&#xff1a;答案满足凸性 题目类似为 n n n 个里面选 m m m 个求某种代价&#xff0c;暴力二维dp复杂度大&#xff0c;但容易计算不限制选的次数。 由于不限制选的次数&#xff0c;所以给选一个东西给一个代价 v v v&#xff0c;然后判断最后选了多少个&#xff0c;再…

leetcode做题笔记93. 复原 IP 地址

有效 IP 地址 正好由四个整数&#xff08;每个整数位于 0 到 255 之间组成&#xff0c;且不能含有前导 0&#xff09;&#xff0c;整数之间用 . 分隔。 例如&#xff1a;"0.1.2.201" 和 "192.168.1.1" 是 有效 IP 地址&#xff0c;但是 "0.011.255.2…

Docker(md版)

Docker 一、Docker二、更换apt源三、docker搭建四、停启管理五、配置加速器5.1、方法一5.2、方法二 六、使用docker运行漏洞靶场1、拉取tomcat8镜像2、拉取成功3、开启服务4、查看kali的IP地址5、访问靶场6、关闭漏洞靶场 七、vulapps靶场搭建 一、Docker Docker是一个开源的应…

VUE3添加全局变量

全局变量的添加 在vue3.0中注入全局方法不是在prototype上挂载了&#xff0c;而是添加在config.globalProperties属性上。 //main.js import { createApp } from "vue"; import App from "./App.vue";const app createApp(App); app.config.globalPrope…

2023国赛数学建模思路 - 案例:随机森林

文章目录 1 什么是随机森林&#xff1f;2 随机深林构造流程3 随机森林的优缺点3.1 优点3.2 缺点 4 随机深林算法实现 建模资料 ## 0 赛题思路 &#xff08;赛题出来以后第一时间在CSDN分享&#xff09; https://blog.csdn.net/dc_sinor?typeblog 1 什么是随机森林&#xff…

使用 DPO 微调 Llama 2

简介 基于人类反馈的强化学习 (Reinforcement Learning from Human Feedback&#xff0c;RLHF) 事实上已成为 GPT-4 或 Claude 等 LLM 训练的最后一步&#xff0c;它可以确保语言模型的输出符合人类在闲聊或安全性等方面的期望。然而&#xff0c;它也给 NLP 引入了一些 RL 相关…