Rust-NLL(Non-Lexical-Lifetime)

news2025/1/11 22:42:14

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Ⅱ语义。

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

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

相关文章

IOC之Spring统一资源加载策略

前言 在学 Java的时候,我们学习了一个标准类 java.net.URL,该类在 Java SE 中的定位为统一资源定位器(Uniform Resource Locator),但是我们知道它的实现基本只限于网络形式发布的资源的查找和定位。然而,实…

Linux环境搭建FastDFS文件服务器(附带Nginx安装)

本文主要介绍在linux服务器如何搭建FastDFS文件服务器。大概分为9个步骤,由于内容较为繁琐。下面带你入坑! 首先简单介绍一下FastDFS是淘宝资深架构师余庆老师主导开源的一个分布式文件系统,用C语言编写。适应与中小企业,对文件不…

【教3妹学编程-算法题】3008. 找出数组中的美丽下标 II

3妹:呜呜,烦死了, 脸上长了一个痘 2哥 : 不要在意这些细节嘛,不用管它,过两天自然不就好了。 3妹:切,你不懂,影响这两天的心情哇。 2哥 : 我看你是不急着找工作了啊, 工作…

Python - 深夜数据结构与算法之 LRUCache

目录 一.引言 二.LRU Cache 简介 1.实现特性 2.工作流程 三.LRU Cache 实战 1.HashMap ListNode 2.OrderedDict 四.总结 一.引言 LRU 即 Least Recently Used 意为最近使用,它是一种局部 Cache 的缓存方法,用于存储最近使用的元素,…

2023.1.15 关于 Redis 持久化 RDB 策略详解

目录 Redis 持久化 Redis 实现持久化的两大策略 RDB 策略 手动触发 save 命令 bgsave 命令 bgsave 命令执行流程 自动触发 rdb 文件 实例演示一 实例演示二 实例演示三 实例演示四 RDB 策略的优缺点 Redis 持久化 什么是持久化? 回答: 将数据存…

高效视频剪辑:视频合并让视频焕然一新,添加背景音乐更动听

随着社交媒体和数字内容的普及,视频剪辑已成为一项常用的技能。除了基本的剪辑技巧外,添加合适的背景音乐也是提升视频质量的方法。下面来看云炫AI智剪的高效视频剪辑技巧——如何批量合并视频,添加动听的背景音乐。 视频合并后的效果展示&a…

Jenkins之pipeline

安装插件 Pipeline Pipeline: Stage View Plugin 创建任务 配置 demo 开始实践 拉取git仓库代码 checkout scmGit(branches: [[name: */main]], extensions: [], userRemoteConfigs: [[url: http://178.119.30.133:8929/root/mytest.git]])通过SonarQube做质量检测 sh …

MAC iterm 显示git分支名

要在Mac上的iTerm中显示Git分支名,您需要使用一个名为“Oh My Zsh”的插件。Oh My Zsh是一个流行的Zsh框架,它提供了许多有用的功能和插件,包括在终端中显示Git分支名。 以下是在iTerm中显示Git分支名的步骤: 1、安装Oh My Zsh&…

芯片烧写工具

问题描述 最近出了一个机器变砖的问题,一些用户使用的设备,头一天晚上用的好好的,第二天来一上电开机就起不来了。 然后就寄回来,返厂维修。一些是因为部分电子器件坏了,还有一些是文件系统问题,重新升级一…

基于Redis+Lua的分布式限流

本文已收录至我的个人网站:程序员波特,主要记录Java相关技术系列教程,共享电子书、Java学习路线、视频教程、简历模板和面试题等学习资源,让想要学习的你,不再迷茫。 前面我们了解了如何利用Nginx做网关层限流&#xf…

XSS漏洞:xss-labs靶场通关

xss系列往期文章: 初识XSS漏洞-CSDN博客 利用XSS漏洞打cookie-CSDN博客 目录 第一关 第二关 第三关 第四关 第五关 第六关 第七关 第八关 第九关 第十关 第十一关 第十二关 第十三关 第十四关 第十五关 第十六关 第十七关 第十八关 第十九关 …

概率论与数理统计————古典概型、几何概型和条件概率

一、古典概型 特点 (1)有限性:试验S的样本空间的有限集合 (2) 等可能性:每个样本点发生的概率是相等的 公式:P(A) A为随机事件的样本点数;S是样本…

深度学习笔记(四)——使用TF2构建基础网络的常用函数+简单ML分类实现

文中程序以Tensorflow-2.6.0为例 部分概念包含笔者个人理解,如有遗漏或错误,欢迎评论或私信指正。 截图和程序部分引用自北京大学机器学习公开课 TF2基础常用函数 1、张量处理类 强制数据类型转换: a1 tf.constant([1,2,3], dtypetf.floa…

Docker部署Jira、Confluence、Bitbucket、Bamboo、Crowd,Atlassian全家桶

文章目录 省流:注意:1.docker-compose文件2.其他服务都正常启动,唯独Bitbucket不行。日志错误刚启动时候重启后查询分析原因再针对第一点排查看样子是安装的bitbucket和系统环境有冲突问题? 结论: 省流: b…

Flink-SQL——时态表(Temporal Table)

时态表(Temporal Table) 文章目录 时态表(Temporal Table)数据库时态表的实现逻辑时态表的实现原理时态表的查询实现时态表的意义 Flink中的时态表设计初衷产品价格的例子——时态表汇率的例子——普通表 声明版本表声明版本视图声明普通表 一个完整的例子测试数据代码实现测试…

MATLAB - 使用 TOPP-RA 求解器生成带约束条件的时间最优轨迹

系列文章目录 前言 本例演示如何生成满足速度和加速度限制的轨迹。该示例使用了 contopptraj 函数,该函数使用可达性分析 (RA) 求解受约束的时间最优路径参数化 (TOPP) 轨迹。 一、示例背景 本例解决的是 TOPP 问题,这是一个机器人问题,其目…

vue3+vite项目构建时报错npm ERR! code EPERMnpm ERR! syscall mkdir...

vscode终端中输入npm create vitelatest vueviteproject1 -- --vue命令后报错 具体报错如下: PS D:\project> npm create vitelatest vueviteproject1 -- --vue >> npm ERR! code EPERM npm ERR! syscall mkdir npm ERR! path D:\node\node_cache\_cac…

磁盘raid1降级后,mdxxx rota发生变化

背景 虚拟机系统盘vda后端使用宿主机ssd盘lvm组raid1,虚拟机内部查看vda磁盘类型(rota=1):机械硬盘,vda后端raid1降级导致磁盘类型降级:rota 0---->1,vda磁盘类型显示不正确,应该是ssd类型(rota=0); 分析 1.基础 1.1 linux磁盘类型 Rota表示磁盘类型:(1)0,表…

收银系统源码-智慧新零售系统框架

智慧新零售系统是一套线下线上打通的收银系统,主要给门店提供含线下收银、线上小程序商城、ERP进销存、精细化会员管理、丰富营销插件等为一体的智慧行业解决方案。智慧新零售系统有合伙人、代理商、商户、门店、收银员/导购员等角色,每个角色有相应的权…

从物联网到数字孪生:智慧社区的未来之路

一、物联网在智慧社区中的应用与挑战 随着科技的飞速发展,物联网技术已经深入到我们生活的方方面面,尤其在智慧社区的建设中发挥着举足轻重的作用。物联网通过连接各种设备和系统,为社区居民提供了更便捷、高效的生活方式,同时也…