Rust-NLL(Non-Lexical-Lifetime)

Rust防范“内存不安全”代码的原则极其清晰明了。

如果你对同一块内存存在多个引用,就不要试图对这块内存做修改;如果你需要对一块内存做修改,就不要同时保留多个引用。

只要保证了这个原则,我们就可以保证内存安全。

它在实践中发挥了强大的作用,可以帮助我们尽早发现问题。这个原则是Rust的立身之本、生命之基、活力之源。

这个原则是没问题的,但是,初始的实现版本有一个主要问题,那就是它让借用指针的生命周期规则与普通对象的生命周期规则一样,是按作用域来确定的。

所有的变量、借用的生命周期就是从它的声明开始,到当前整个语句块结束。

这个设计被称为Lexical Lifetime,因为生命周期是严格和词法中的作用域范围绑定的。

这个策略实现起来非常简单,但它可能过于保守了,某些情况下借用的范围被过度拉长了,以至于某些实质上是安全的代码也被阻止了。

在某些场景下,限制了程序员的发挥。

因此,Rust核心组又决定引入Non Lexical Lifetime,用更精细的手段调节借用真正起作用的范围。
这就是NLL。

NLL希望解决的问题

首先,我们来看几个简单的示例。

在这里插入图片描述
这段代码是没有问题的。我们的关注点是foo()这个函数,它在调用capitalize函数的时候,创建了一个临时的&mut型引用,在它的调用结束后,这个临时的借用就终止了,因此,后面我们就可以再用data去修改数据。

注意,这个临时的&mut引用存在的时间很短,函数调用结束,它的生命周期就结束了。

但是,如果我们把这段代码稍作修改,问题就出现了:

在这里插入图片描述
在这段代码中,我们创建了一个临时变量slice,保存了一个指向data的smut型引用,然后再调用capitalize函数,就出问题了。编译器提示为:

error[E0499]:cannot borrow `data’as mutable more than once at a time

这是因为,Rust规定“共享不可变,可变不共享”,同时出现两个&mut型借用是违反规则的。

在编译器报错的地方,编译器认为slice依然存在,然而又使用data去调用fnpush(&mut self,value:T)方法,必然又会产生一个&mut型借用,这违反了Rust的原则。

在目前这个版本中,如果我们要修复这个问题,只能这样做:
在这里插入图片描述
我们手动创建了一个代码块,让slice在这个子代码块中创建,后面就不会产生生命周期冲突问题了。

这是因为,在早期的编译器内部实现里面,所有的变量,包括引用,它们的生命周期都是从声明的地方开始,到当前语句块结束(不考虑所有权转移的情况)。

这样的实现方式意味着每个引用的生命周期都是跟代码块(scope)相关联的,它总是从声明的时候被创建,在退出这个代码块的时候被销毁,因此可以称为Lexical lifetime。

而所说的Non-Lexical lifetime,意思就是取消这个关联性,引用的生命周期,我们用另外的、更智能的方式分析。

有了这个功能,上例中手动加入的代码块就不需要了,编译器应该能自动分析出来,slice这个引用在capitalize函数调用后就再没有被使用过了,它的生命周期完全可以就此终止,不会对程序的正确性有任何影响,后面再调用push方法修改数据,其实跟前面的slice并没有什么冲突关系。

看了上面这个例子,可能有人还会觉得,显式的用一个代码块来规定局部变量的生命周期是个更好的选择,Non-Lexical-Lifetime的意义似乎并不大。

那我们再继续看看更复杂的例子。我们可以发现,Non-Lexical-Lifetime可以打开更多的可能性,让用户有机会用更直观的方式写代码。比如下面这样的一个分支结构的程序:

在这里插入图片描述
这段代码从一个HashMap中查询某个key是否存在。

如果存在,就继续处理,如果不存在,就插入一个新的值。

目前这段代码是编译不过的,因为编译器会认为在调用getmut(&key)的时候,产生了一个指向map的amut型引用,而且它的返回值也包含了一个引用,返回值的生命周期是和参数的生命周期一致的。

这个方法的返回值会一直存在于整个match语句块中,所以编译器判定,针对map的引用也是一直存在于整个match语句块中的。

于是后面调用insert方法会发生冲突。

当然,如果我们从逻辑上来理解这段代码,就会知道,这段代码其实是安全的。

因为在None分支,意味着map中没有找到这个key,在这条路径上自然也没有指向map的引用存在。

但是可惜,在老版本的编译器上,如果我们希望让这段代码编译通过,只能绕一下。我们试一下做如下的修复:

在这里插入图片描述
实际上这个改动依然会编译失败。

原因就在于return语句,get_mut时候对map的借用传递给了Some(value),在Some这个分支内存在一个引用,指向map的某个部分,而我们又把value返回了,这意味着编译器认为,这个借用从match开始一直到退出这个函数都存在。因此后面的insert调用依然发生了冲突。

接下来我们再做一次修复:
在这里插入图片描述
这次的区别在于,get_mut发生在一个子语句块中。

在这种情况下,编译器会认为这个借用跟if外面的代码没什么关系。

通过这种方式,我们终于绕过了borrow checker。

但是,为了绕过编译器的限制,我们付出了一些代价。

这段代码,我们需要执行两次hash查找,一次在contains方法,一次在get_mut方法,因此它有额外的性能开销。

这也是为什么标准库中的HashMap设计了一个叫作entry的api,如果用entry来写这段逻辑,可以这么做:
在这里插入图片描述
这个设计既清晰简洁,也没有额外的性能开销,而且不需要Non-Lexical-Lifetime的支持。

这说明,虽然老版本的生命周期检查确实有点过于严格,但至少在某些场景下,我们其实还是有办法绕过去的,不一定要在“良好的抽象”和“安全性”之间做选择。

但是它付出了其他的代价,那就是设计难度更高,更不容易被掌握。

标准库中的entry API也是很多高手经过很长时间才最终设计出来的产物。

对于普通用户而言,如果在其他场景下出现了类似的冲突,恐怕大部分人都没有能力想到一个最佳方案,可以既避过编译器限制,又不损失性能。

所以在实践中的很多场景下,普通用户做不到“零开销抽象”。

让编译器能更准确地分析借用指针的生命周期,不要简单地与scope相绑定,不论对普通用户还是高阶用户都是一个更合理、更有用的功能。如果编译器能有这么聪明,那么它应该能理解下面这段代码其实是安全的:

在这里插入图片描述这段代码既符合用户直观思维模式,又没有破坏Rust的安全原则。

以前的编译器无法编译通过,实际上是对正确程序的误伤,是一种应该修复的缺陷。

NLL的设计目的就是让Rust的安全检查更加准确,减少误报,使得编译器对程序员的掣肘更少。
打开NLL功能,以下代码就可以编译通过了:
在这里插入图片描述

NLL的原理

NLL的设计目的是让“借用”的生命周期不要过长,适可而止,避免不必要的编译错误,把实际上正确的代码也一起拒绝掉。

但是实现方法不能是简单地在AST上找以下某个引用最后一次在哪里使用,就让它的生命周期结束算了。

我们用例子来说明:
在这里插入图片描述
在这个示例中,我们引入了一个循环结构。如果我们只是分析AST的结构的话,很可能会觉得capitalize函数结束后,slice的生命周期就结束了,因此data.push()方法调用是合理的。

但这个结论是错误的,因为这里有一个循环结构。大家想想看,如果执行了push()方法后,引发了vec数据结构的扩容,它把以前的空间释放掉,申请了新的空间,进入下一轮循环的时候,slice就会指向一个非法地址,会出现内存不安全。

以上这段代码理应出现编译错误。

因此,新版本的借用检查器将不再基于AST的语句块来设计,而是将AST转换为另外一种中间表达形式MIR(middle-level intermediate representation)之后,在MIR的基础上做分析。

这是因为前面已经分析过了,对于复杂一点的程序逻辑,基于AST来做生命周期分析是费力不讨好的事情,而MIR则更适合做这种分析。

可以用以下编译器命令打印出MIR的文本格式:
rustc --emit=mir test.rs
不过在一般情况下,MIR在编译器内部的表现形式是内存中的一组数据结构。

这些数据结构描述的是一个叫作“控制流图”(control flow graph)的概念。

所谓控制流图,就是用“图”这种数据结构,描述程序的执行流程。每个函数都有一个MIR来描述它,比如对于以下这段代码:

在这里插入图片描述
在这里插入图片描述

图上面有节点,也有边。节点代表一条或者一组语句,边代表分支跳转。有了这个图,引用的生命周期就可以用这个图上的节点来表示了。

编译器最后会分析出来,引用在这个图上的哪些节点上还是活着的,在哪些节点上可以看作已经死掉了。

相比于以前,一个引用的生命周期直接充满整个语句块,现在的表达方式明显要精细得多,这样我们就可以保证引用的生命周期不会被过分拉长。

这个新版的借用分析器,会允许下面的代码编译成功,比如:
在这里插入图片描述
另外需要强调的是:

  • 这个功能只影响静态分析结果,不影响程序的执行情况;
  • 以前能编译通过的程序以后依然会编译通过,不会影响以前的代码;
  • 它依然保证了安全性,只是将以前过于保守的检查规则适当放宽;
  • 它依赖的依然是静态检查规则,不会涉及任何动态检查规则;
  • 它只影响“引用类型”的生命周期,不影响“对象”的生命周期,即维持现有的析构。
  • 它不会影响RAⅡ语义。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

入门Docker1: 容器技术的基础

目录 服务器选型 虚拟机 基于主机(物理机或虚机)的多服务实例 基于容器的服务实例 Docker Docker三要素 Docker安装 Docker基本使用 基本操作 仓库镜像 容器 服务器选型 在选择服务器操作系统时, Windows 附带了许多您需要付费的功能。 Linux 是开放源代…

光K8S的目录结构就够你学一天!

Kubernetes Project Layout设计 Kubernetes项目由Go语言编写。Go语言官方对项目的结构设计没有强制要求,早期的Go语言开发者都喜欢将包文件代码放置在项目的src/目录下,如nsqio开源项目,开发者喜欢将入口文件放入apps/目录。不同开发者的喜好…

使用WAF防御网络上的隐蔽威胁之CSRF攻击

在网络安全领域,除了常见的XSS(跨站脚本)攻击外,CSRF(跨站请求伪造)攻击也是一种常见且危险的威胁。这种攻击利用用户已经验证的身份在没有用户知情的情况下,执行非授权的操作。了解CSRF攻击的机…

运筹说 第45期丨多目标规划发展及其提出者—— Abraham Charnes和William W. Cooper

经过前面的学习,相信大家已经对运筹学的运输问题有了更加全面的了解,接下来小编将带你学习新一章的内容, 先来看看多目标规划的发展简史,然后再带你领略该理论两位提出者的传奇一生! 01目标规划发展简史 Vilfredo Pa…

Vue2.组件通信

样式冲突 写在组件中的样式默认会全局生效。容易造成多个组件之间的样式冲突问题。 可以给组件加上scoped属性,让样式只作用于当前组件。 原理: 给当前组件模板的所有元素,加上一个自定义属性data-v-hash值,用以区分不同的组件。…

【Axure高保真原型】移入放大对应区域的饼图

今天和大家分享移入放大对应扇形区域的饼图的原型模板,鼠标移入时,对应扇形区域的会放大,并且的项目和数据弹窗,弹窗可以跟随鼠标移动。这个原型是用Axure原生元件制作的,所以不需要联网或者调用外部图表……具体效果可…

一篇教你生成密钥给自己打的exe添加密钥

一篇教你生成密钥给自己打的exe添加密钥 我这里是自己写了一个python 打包exe,说总是给我报毒什么的 文章目录 一篇教你生成密钥给自己打的exe添加密钥前言一、使用java jdk 自带的keytool?二、进行转换2.把证书密钥写入到你的exe 总结 前言 生成密钥并为自定义 .…

上海市税务局:买卖虚拟货币需缴税!中国仍未有放松加密政策的迹象?

自2021年央行等十部委下发禁止虚拟货币交易的通知以来,国内虚拟货币交易平台几乎销声匿迹。然而,最近一则关于个人所得税的释义再次引起了人们的关注。 1月5日,国家税务总局上海市税务局在官方公众号发布《个人所得税经营所得和分类所得常见误…

Win10专业版系统搭建DNS解析服务

Win10专业版 纯新手,也没弄过Linux的。不喜勿喷,有问题请指出 第一天一头雾水整了几个小时没结果,第二天豁然开朗,10分钟明白了第一天的问题所在。 Win10 安卓: iOS: 搭建DNS服务器的意义: 屏蔽…

关于Python —— Python教程

开始 Python 是一个易于学习、使用和高效阅读的编程语言。它具有简洁的英文语法,编写更少的代码,让程序员专注于业务逻辑而不是语言本身。 本教程将从深度、专注细节上去理解 Python 这门语言。初学者可以参考此教程理解相应的内容,本教程将…

高精度磁导航传感器MGS系列RS232|RS485|CANBUS通讯连线方法

高精度磁导航传感器MGS系列,包含:CNS-MGS-080N、CNS-MGS-160N等,具有1mm的检测精度,特别适应于⾼精度磁条导航。利⽤检测磁场相对位置来进⾏AGV的辅助定位对接,获得更⾼的导航、定位、驻⻋精度。 MGS系列磁导航传感器⽀…

Mysql中设置只允许指定ip能连接访问(可视化工具的方式)

场景 Mysql中怎样设置指定ip远程访问连接: Mysql中怎样设置指定ip远程访问连接_navicat for mysql 设置只有某个ip可以远程链接-CSDN博客 前面设置root账户指定ip能连接访问是通过命令行的方式,如果通过可视化工具比如Navicat来实现。 注&#xff1a…

360度全景展示效果图怎么制作?

全景图如何做成360度可以观看的效果呢? ​前提是我们要做好单张的全景效果图!往往全景图整体有两种以下生成方式。 方法一:全景相机拍摄 这个需要有专业制作全景的相机设备,拍摄实地的全景图,这样的效果出色&#xff…

微信小程序之初步了解微信小程序

学习的最大理由是想摆脱平庸,早一天就多一份人生的精彩;迟一天就多一天平庸的困扰。各位小伙伴,如果您: 想系统/深入学习某技术知识点… 一个人摸索学习很难坚持,想组团高效学习… 想写博客但无从下手,急需…

鸿蒙开发(三)理解UIAbility

前文提到过,在使用DevEco创建鸿蒙项目的时候,会选择Empty Ability,那么这个Ability是什么呢?其实对比Android Studio创建Android羡慕时选择的Empty Activity,感觉Harmony的Ability更像是Android的Activity,…

高效微调大型预训练模型的Prompt Learning方法

目录 前言1 prompt learning简介2 prompt learning步骤2.1 选择模型2.2 选择模板(Template)2.3 Verbalizer的构建 3 Prompt Learning训练策略3.1 Prompting组织数据,优化参数3.2 增加Soft Prompts,冻结模型,优化Prompt…

参与直播领取龙年大礼盒!23年Coremail社区年终福利大放送

2023年终福利大放送 Coremail 管理员社区是由 Coremail 邮件安全团队、服务团队及多条产品线共同维护,集 7*24h 在线自助查询、技术问答交流、大咖互动分享、资料下载等功能于一体,专属于 Coremail 邮件管理员、安全员成长互动的知识库社区。 转眼间&am…

详解电源动态响应的测试方法及重要性 -纳米软件

电源动态响应测试的重要性 电源动态响应测试是为了检测电源系统在负载变化、输入电压变化情况下的性能表现,包括响应速度、稳定性以及恢复能力等,从而判断电源能否快速、准确地恢复到正常工作状态,为电源的优化设计提供依据。 动态响应能力影…

2024谷歌SEO自学基础入门

2024年可能会迎来大航海时代,国内各企业也加速了出海的步伐!! (看总额,今年中国跨境电商,前三季度进出口1.7万亿元人民币,创造了14.4%的增长。 看体量,过去五年,中国跨…