Rust 生命周期浅谈

1. 简述

image-20240504202148065

Rust 中的每一个引用都有其 生命周期lifetime),也就是引用保持有效的作用域。大部分时候生命周期是隐含并可以推断的,正如大部分时候类型也是可以推断的一样。类似于当因为有多种可能类型的时候必须注明类型,也会出现引用的生命周期以一些不同方式相关联的情况,所以 Rust 需要我们使用泛型生命周期参数来注明他们的关系,这样就能确保运行时实际使用的引用绝对是有效的。

生命周期的概念从某种程度上说不同于其他语言中类似的工具,毫无疑问这是 Rust 最与众不同的功能。


2. 秒懂生命周期

生命周期就是一个用来避免出现悬垂引用的手段,本质上就是约束和说明变量作用域的作用关系,更好的避免哪些已经失效的数据再次被引用从而导致的一些列问题。

什么是非法引用呢?看下面这个例子:

fn main() {let r;{let x = 5;r = &x;}println!("r: {}", r);}
}
  • 实例代码中,我们在代码块的外部定义一个变量r,并在后续的代码块中定义一个变量x且赋值为5之后将变量x的引赋给前面r,到这里其实没什么问题。继续往下,在代码块之后将r的值打印输出,此时是无法通过编译的因为这已经出现了非法引用的问题,也就是所谓 悬垂引用
  • 这是因为x变量在执行赋值之后,截至代码#6行开始,它的作用域就结束了,也就是说,x变量的生命周期到此为止,但由于后续还存在打印r的操作,而此时由于x的结束,r所指向的数据就是一个不存在的东西,那不得报错啊。那能编译通过的话就属于玄学了。

变量 x 并没有 “存在的足够久”。其原因是 x 在到达第 7 行内部作用域结束时就离开了作用域。不过 r 在外部作用域仍是有效的;作用域越大我们就说它 “存在的越久”。

那么Rust编译器是如何直判断这段代码不能通过编译的呢?其实很简单,看的就是哪个变量的作用域存在时间更长。当然,官方将这种方式起名叫做 借用检查器

他的作用就是通过比较作用域来确保借用的合法性,避免悬垂。

image-20240504183054170

上图还是之前的示例,我使用不同的颜色以及生命周期标记来指出了变量xr的作用域,或者说生命周期时长。

  • 'a'也就是红色部分表示r的生命周期;
  • 'b'也就是亮绿色的部分表示x的生命周期;

这样就可以直观的感受到内部的 'b 块要比外部的生命周期 'a 小得多。Rust的借用检查器在编译时就会发现r引用了一个生命周期小于自己的变量x,被引用的对象比它的引用者存在的时间更

假如r在后续还需要带着x一起干一番大事业。但是发现x在这之前就西天取经去了,r也只能放弃了这个想法,人生到此结束。

换句话说,在借用关系中,被借用的对象生命周期必须大于等于借用者的生命周期,否则会出现借用者借用之后被借用的对象挂了,那借用者借了个寂寞,Rust直接拒绝编译。

所以,依据上面的原理,将代码作适当的调整之后就可以正常编译了,像下面这样。此时被借用的x的生命周期为'b且大于借用者r'a,不会出现非法借用的问题。

image-20240504184449312


3. 函数中泛型生命周期

故事还得从一个简单的方法讲起。

fn main() {let string1 = String::from("abcd");let string2 = "xyz";let result = longest(string1.as_str(), string2);println!("The longest string is {}", result);
}

从上面的内容不难猜测,函数longest()的作用是返回两个切片中较长的一个,功能就这么简单!

参考下面的函数实现,这种写法能逃过编译器的考验成功通过编译吗?

fn longest(x: &str, y: &str) -> &str {if x.len() > y.len() {x} else {y}
}

乍一看没问题啊,不就是传入两个字符串引用比较长短返回吗,为了保留实参的所有权还特地将函数参数使用了引用方式传递呢。写的挺板正的啊,语法简洁,逻辑清晰。但还是禁不住编译器的百般拷打,终于还是露出了狐狸尾巴。

image-20240504185825600

函数尝试返回 xy 的引用,但是这两个参数的生命周期并没有明确定义。在函数返回时,编译器无法确定返回的引用是否仍然有效。这和之前例子不太一样的地方就是我们没办法直观(抽象一点也可以啊)的看出来x,y的作用域,没办法确定生命周期时长,基于这个原理,Rust的借用检查器也做不到这一点。

为了解决这个问题,就需要使用泛型生命周期参数来明确指定返回引用的生命周期与输入参数的生命周期之间的关系。

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {if x.len() > y.len() {x} else {y}
}

此时代码正常执行:

image-20240504190734366

在修复后的代码中,我们使用了泛型生命周期参数 'a,这样可以确保返回的引用与输入参数的生命周期相匹配。这样编译器就能够正确推断返回引用的生命周期,避免悬垂引用或生命周期不匹配的问题。

  • 生命周期标注有着一个不太常见的语法:生命周期参数名称必须以撇号(')开头,其名称通常全是小写,类似于泛型其名称非常短。'a 是大多数人默认使用的名称。

  • 生命周期标注描述了多个引用生命周期相互的关系,而不影响其生命周期。

  • 生命周期参数标注位于引用的 & 之后,并有一个空格来将引用类型与生命周期标注分隔开。

&i32        // 引用
&'a i32     // 带有显式生命周期的引用
&'a mut i32 // 带有显式生命周期的可变引用

看到这儿,你大概还是一知半解、一头雾水、一脸懵逼、一愣一愣。不着急,等我去画个图先,人的脑子总是惯性的偏向于理解图像信息而不是文字,尽管我文采飞扬,满屏生花!


3.1 再论泛型生命周期

通过上面泛型生命周期的简单使用大概可以获取到下面这些信息:

  • 此时通过函数签名可以明确某些生命周期'a,在函数获取到的两个参数中他们的生命周期都是和'a保持一致,对于返回值也是一个道理,也就是说,此时不论是两个参数x,y还是返回值都保持了生命周期的大小同步。
  • 怎么理解这个 同步的含义是重点,这就又和上面所学的东西关联上了,所谓的同步,就是这个生命周期标识'a会保证参数和返回值将会是三者中生命周期的较小者,可以理解为三者的交集,这也是我们需要告知rust需要保证的某种约束条件。
  • 在函数执行时,当具体的引用被传入到该函数中,'a标记的生命周期就是两个引用参数x,y的较小者(为什么不是较大者,请回去再看一遍上一个目录的内容)。
  • 保证 了x,y的约束条件之后,最终函数在返回值时,还需要再次保证此时返回值的生命周期和之前两个引用参数的生命周期的较小者。

image-20240504193644091

如上图所示。

  • 我们假设两个参数的生命周期为其较小的一方(假设为z),那么z = min(x,y);
  • w表示返回值的生命周期,那么最终返回的生命周期为min(z,w)
  • 他们之间类似于数学概念上的交集的定义,只有保证了全部生命周期中的重叠部分一致,才能保证整个函数生命周期的有效性,但凡取一个较大或者较小的值,都可能会导致非法引用问题的出现。

需要注意的是,生命周期标识仅仅作为一种标识,它本身没有更多的实际意义,也不会直接影响某个函数的功能,仅作为一种约束关系的表示而已。

这些标注出现在函数签名中,而不存在于函数体中的任何代码中。这是因为 Rust 能够分析函数中代码而不需要任何协助,不过当函数引用或被函数之外的代码引用时,让 Rust 自身分析出参数或返回值的生命周期几乎是不可能的。这些生命周期在每次函数被调用时都可能不同。这也就是为什么我们需要手动标记生命周期的原因。


理论部分巴拉完了,下面通过两个具体的例子,来直观感受下如何通过传递拥有不同具体生命周期的引用来限制 longest 函数的使用。

函数还是之前的函数,请注意观察main方法中的内容:

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {if x.len() > y.len() {x} else {y}
}fn main() {let string1 = String::from("long string is long");{let string2 = String::from("xyz");let result = longest(string1.as_str(), string2.as_str());println!("The longest string is {}", result);}
}

输出: The longest string is long string is long

这个例子中,string1的作用域显然大于string2,所以它直到整个外部作用域结束都是有效的,string2则只在{}代码块中有效,作用域较小。

result这是引用了哪些直到内部作用域结束时也还有效的值,这就相当于在string1string2中取了交集部分,二者的较小值,此时借用检查器正常检查通过,所以会看到那段输出。

没有比对就没有对比,看看下面这个例子:

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {if x.len() > y.len() {x} else {y}
}fn main() {let string1 = String::from("long string is long");let result;{let string2 = String::from("xyz");result = longest(string1.as_str(), string2.as_str());}println!("The longest string is {}", result);
}

这个例子中:

  • string1直到外部作用域结束都是有效的

  • string2的作用域只在内部代码块中有效,显然在作用域范围上满足string2<string1

  • 与上一个例子比较,这里将result的声明移到了代码块之外,也即是内部作用域之外,但是它和string2的赋值操作还是留在代码块中

  • 并且打印result的代码也移到了代码块之外

通过上面的分析,这段代码显然是无法通过编译器拷打的,所以你才会看到下面的异常提示:

image-20240504195414309

  • 从人的角度读上述代码,我们可能会觉得这个代码是正确的。 string1 更长,因此 result 会包含指向 string1 的引用。因为 string1 尚未离开作用域,对于 println! 来说 string1 的引用仍然是有效的。然而,我们通过生命周期参数告诉 Rust 的是: longest 函数返回的引用的生命周期应该与传入参数的生命周期中较短那个保持一致。
  • 基于上面 保持一致 这一点,此时就应该取string2作为最终的生命周期,因为它显然比string1短,但由于此时string2在离开代码块之后就已经失效了,导致在 println! 中尝试使用 result 时,string2 已经被丢弃,从而产生了悬垂引用。是无法通过借用检查器的检查的,此时编译器收到了检查器的眼神之后,二话不说上来就是一大嘴巴子,并甩出了一句:“拒绝编译!!”

4. 参考&引用

  • 《Rust权威指南》

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

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

相关文章

Codeforces Round 943 (Div. 3) G2. Division + LCP (hard version)

题目 思路&#xff1a; #include <bits/stdc.h> using namespace std; //#define int long long 开这个会TLE #define __int128 long long #define pb push_back #define fi first #define se second #define lson p << 1 #define rson p << 1 | 1 const in…

FFmpeg学习记录(二)—— ffmpeg多媒体文件处理

1.日志系统 常用的日志级别&#xff1a; AV_LOG_ERRORAV_LOG_WARNINGAV_LOG_INFOAV_LOG_DEBUG #include <stdio.h> #include <libavutil/log.h>int main(int argc, char *argv[]) {av_log_set_level(AV_LOG_DEBUG);av_log(NULL, AV_LOG_DEBUG, "hello worl…

【网站项目】社区互助平台

&#x1f64a;作者简介&#xff1a;拥有多年开发工作经验&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的项目或者毕业设计。 代码可以私聊博主获取。&#x1f339;赠送计算机毕业设计600个选题excel文件&#xff0c;帮助大学选题。赠送开题报告模板&#xff…

Java | AI+编程 | 如何使用通义灵码提升开发效率

大家好&#xff0c;我是程序员影子 | 全网同名 一名致力于帮助更多朋友快速入门编程的程序猿 今天&#xff0c;我将以小白入门的视角带着大家学会如何在Idea上使用通义灵码&#xff0c;提高开发效率&#xff0c;减少重复工作&#xff1b;话不多说&#xff0c;我们直接进入正题…

深入解析I2C协议:通讯简化之道

在现代电子系统中&#xff0c;组件间的通信是必不可少的。而I2C协议&#xff08;Inter-Integrated Circuit&#xff09;&#xff0c;由Philips Semiconductor&#xff08;现为NXP Semiconductors&#xff09;在1980s初期发明&#xff0c;已成为一种广泛使用的串行通信协议。其设…

Endnote X9 20 21如何把中文引用的et al 换(变)成 等

描述 随着毕业的临近&#xff0c;我在写论文时可能会遇到在引用的中文参考文献中出现“et al”字样。有的学校事比较多&#xff0c;非让改成等等&#xff0c;这就麻烦了。 本身人家endnote都是老美的软件&#xff0c;人家本身就是针对英文文献&#xff0c;你现在让改成等等&a…

揭秘Fabric交易流程:一文带你深入了解

随着区块链技术的日益普及&#xff0c;Hyperledger Fabric作为一种联盟链解决方案&#xff0c;受到了广泛关注。那么&#xff0c;Fabric的交易流程究竟是怎样的呢&#xff1f;本文将为您一一揭晓。 1. Fabric交易的参与方 客户端&#xff1a;交易流程的发起方&#xff0c;发起…

面试中算法(最大公约数)

高效求出两个整数的最大公约数&#xff0c;要尽量优化算法的性能。 def getDiv(a,b):mamax(a,b)mimin(a,b)#判断能被整除if ma%mi0:return mi#递归return getDiv(ma%mi,mi)if __name__ __main__:# print(getDiv(10, 25))print(getDiv(1000, 50))没错&#xff0c;这确实是辗转…

12【PS作图】像素画技巧

【内容背景】本节介绍PS画像素画的小技巧&#xff0c;能够节约绘画时间~ 直线 斜线 画直线&#xff0c;长按 Shift 键&#xff0c;然后用铅笔画就可以画出水平线、垂直线和斜线 &#xff08;1&#xff09;如果按住shift键&#xff0c;一直按住鼠标&#xff0c;就可以得到水平…

“视频号小店”和“抖音小店”新手做电商选择哪个更好?

哈喽~我是电商月月 做电商的老商家和&#xff0c;准备做电商的新手朋友都知道现在最大的电商平台就是“抖音小店” 但抖店小店毕竟发展了四年&#xff0c;流量多&#xff0c;商家也多&#xff0c;最近又崛起了一个新黑马“视频号小店” 那到底去哪个平台发展才有前景呢&…

基于51单片机的智能台灯proteus仿真设计( proteus仿真+程序+原理图+报告+讲解视频)

基于51单片机的红外光敏检测智能台灯控制系统仿真( proteus仿真程序原理图报告讲解视频&#xff09; 1.主要功能&#xff1a; 基于51单片机的红外检测光照检测智能台灯仿真设计 1、检测光照强度并显示在数码管上。 2、具备红外检测人体功能。 3、灯光控制模式分为自动模式…

【强训笔记】day8

NO.3 思路&#xff1a;相乘除以最大公约数等于最小公倍数。最小公倍数等于gcd&#xff08;a&#xff0c;a%b&#xff09;递归直到b等于0。 代码实现&#xff1a; #include <iostream> using namespace std;int gcd(int a,int b) {if(b0) return a;return gcd(b,a%b); }…

关于YOLO8学习(六)安卓部署ncnn模型--图片检测

前文 关于YOLO8学习(一)环境搭建,官方检测模型部署到手机 关于YOLO8学习(二)数据集收集,处理 关于YOLO8学习(三)训练自定义的数据集 关于YOLO8学习(四)模型转换为ncnn 关于YOLO8学习(五)安卓部署ncnn模型–视频检测 简介 前文第五章,讲述了部署自定义模型后,进…

2023第十四届蓝桥杯国赛C/C++ 大学 A 组 圆上的连线

思路&#xff1a;很显然总的方案数等于挑选偶数点的方案数乘以对应偶数点的连线方案数之和&#xff0c;挑选偶数点的方案数靠组合数得出&#xff0c;偶数点的连线方案数就是个卡特兰数。具体为什么是卡特兰数&#xff0c;可以任选一个点&#xff0c;枚举这个点所连边的位置&…

FloodFill-----洪水灌溉算法(DFS例题详解)

目录 一.图像渲染&#xff1a; 代码详解&#xff1a; 二.岛屿数量&#xff1a; 代码详解&#xff1a; 三.岛屿的最大面积&#xff1a; 代码详解&#xff1a; 四.被围绕的区域&#xff1a; 代码详解&#xff1a; 五.太平洋大西洋水流问题&#xff1a; 代码详解&#x…

大语言模型从Scaling Laws到MoE

1、摩尔定律和伸缩法则 摩尔定律&#xff08;Moores law&#xff09;是由英特尔&#xff08;Intel&#xff09;创始人之一戈登摩尔提出的。其内容为&#xff1a;集成电路上可容纳的晶体管数目&#xff0c;约每隔两年便会增加一倍&#xff1b;而经常被引用的“18个月”&#xf…

C++学习第二十二课:STL映射类的深入解析

C学习第二十二课&#xff1a;STL映射类的深入解析 在C标准模板库&#xff08;STL&#xff09;中&#xff0c;映射类&#xff08;std::map和std::multimap&#xff09;是用来存储关联数据的容器。与集合类不同&#xff0c;映射类中的每个元素都是一个键值对&#xff08;key-val…

关于继承~

继承 动物有猫、狗&#xff0c; 猫又分为加菲猫、布偶猫......&#xff1b;狗又有哈士奇、德国牧羊犬...... 我们发现&#xff0c;下一类除了拥有上一类的共性之外&#xff0c;还拥有自己的特性。 于是我们可以利用继承的方式来减少重复的代码 继承的基本语法 class A:p…

buuctf re findKey

参考&#xff1a;http://t.csdnimg.cn/hUKRJ 参考&#xff1a;http://t.csdnimg.cn/kIk4i 32位&#xff0c;ida打开 f5&#xff0c;先不管呢 winmain函数 看看字符串 进到flag那里&#xff0c;没法反编译 寻找 看汇编&#xff0c;发现两个一样的push&#xff08;不理解怎…

【力扣】203、环形链表 II

142. 环形链表 II 要解决这道题&#xff0c;首先需要对问题进行拆解&#xff1a; 确定链表是否存在环确定环的入口点 如何判断是否存在环呢&#xff1f;这个比较容易想到&#xff0c;使用快慢指针即可判断链表是否存在环。我们定义两个指针&#xff1a; ListNode slow head…