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权威指南》

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

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

相关文章

JAVA语言开发的智慧城管系统源码:技术架构Vue+后端框架Spring boot+数据库MySQL

通过综合应用计算机技术、网络技术、现代通信技术等多种信息技术&#xff0c;充分融合RS遥感技术、GPS全球定位技术、GIS地理信息系统&#xff0c;开始建设一个动态可视的、实时更新的、精细量化的城市管理系统。智慧城管将采用云平台架构方式进行建设&#xff0c;基于现有数字…

【idea-sprongboot项目】SSH连接云服务器进行远程开发

继上一篇博客【阿里云服务器】ubuntu 22.04.1安装docker以及部署java环境-CSDN博客 目录 五、远程开发方式 1&#xff09;SSH进行远程开发 步骤 配置文件同步 window电脑远程操控 正式通过window电脑远程操控 运行在linux服务器上的远程程序 调试在linux服务器上的远程程…

恶补《操作系统》5_2——王道学习笔记

5.2_1 I-O核心子系统 1、用户层软件 假脱机系统 2、设备独立性软件&#xff08;设备无关性软件&#xff09; IO调度、设备保护、设备分配与回收、缓冲区管理 3、设备驱动程序&#xff08;比如打印机驱动&#xff09; 4、中断处理程序 5、硬件 5.2_2 假脱机技术&#xff…

PHP医疗不良事件上报系统源码 AEMS开发工具vscode+ laravel8 医院安全(不良)事件报告系统源码 可提供演示

PHP医疗不良事件上报系统源码 AEMS开发工具vscode laravel8 医院安全&#xff08;不良&#xff09;事件报告系统源码 可提供演示 医院安全不良事件报告系统&#xff08;AEMS&#xff09;&#xff1b;分为外部报告系统和内部报告系统两类。内部报告系统主要以个人为报告单位&…

智慧文旅开启沉浸式文化体验,科技让旅行更生动:借助智慧技术,打造沉浸式文化体验场景,让旅行者在旅行中深度感受文化的魅力

一、引言 随着科技的飞速发展&#xff0c;传统旅游行业正经历着前所未有的变革。智慧文旅&#xff0c;作为一种新兴的旅游模式&#xff0c;正以其独特的魅力&#xff0c;吸引着越来越多的旅行者。智慧文旅不仅改变了人们的旅行方式&#xff0c;更在深度上丰富了人们的文化体验…

linux上如何排查JVM内存过高?

怎么排查JVM内存过高&#xff1f; 前言&#xff1a; 想必工作一两年以后的同学都会逐渐面临到&#xff0c;jvm等问题&#xff0c;但是可能苦于无法熟练的使用一些工具&#xff1b;本文将介绍几个比较常用分析工具的使用方法&#xff0c;带着大家一步步定位分析问题。 1、top 查…

代码随想录算法训练营DAY54|C++动态规划Part15|647.回文子串、516最长回文子序列、

文章目录 647.回文子串思路CPP代码双指针 516最长回文子序列思路CPP代码 动态规划总结篇 647.回文子串 力扣题目链接 文章链接&#xff1a;647.回文子串 视频链接&#xff1a;动态规划&#xff0c;字符串性质决定了DP数组的定义 | LeetCode&#xff1a;647.回文子串 其实子串问…

【C++第八课 - string的底层实现】

目录 基础知识string构造函数和析构函数的坑构造函数析构函数 迭代器、范围for运算符重载operator [] const增删查改push_backreserveappendinserteraseswapfindsubstr拷贝构造 流插入和流提取<<流插入>>流提取clear 深浅拷贝传统写法现代写法 赋值传统写法现代写法…

## 01深度学习介绍与安装PyTorch

文章目录 深度学习的发展历史和基本概念早期历史兴起与发展基本概念 如何安装和设置PyTorch环境系统要求安装步骤验证安装 结语 深度学习的发展历史和基本概念 深度学习&#xff0c;一种通过使用具有多层结构的神经网络来学习数据的复杂模型的机器学习技术&#xff0c;近年来已…

[Java EE] 多线程(七): 锁策略

&#x1f338;个人主页:https://blog.csdn.net/2301_80050796?spm1000.2115.3001.5343 &#x1f3f5;️热门专栏:&#x1f355; Collection与数据结构 (90平均质量分)https://blog.csdn.net/2301_80050796/category_12621348.html?spm1001.2014.3001.5482 &#x1f9c0;Java …

奇偶校验码

目录 前言 校验原理简介 奇偶校验码 前言 在前两个文章的学习中,我们已经知道了数字字符这些简单的数据应该怎么在计算机内部进行表示,其实本质上是0101的二进制代码,但是这些数据在计算机内部进行计算存取和传送的过程中,由于计算机原器件可能会发生故障,也有可能因为某些…

python:set(集合)

set(集合) 去重处理&#xff0c;内容无序 列表使用&#xff1a;[] 元组使用&#xff1a;() 字符串使用&#xff1a;"" 集合使用&#xff1a;{} 基本语法; # 定义字面量集合&#xff1a;{元素&#xff0c;元素&#xff0c;元素&#xff0c;.......} 定义集合变…

【C语言】项目实践-贪吃蛇小游戏(Windows环境的控制台下)

一.游戏要实现基本的功能&#xff1a; • 贪吃蛇地图绘制 • 蛇吃食物的功能 &#xff08;上、下、左、右方向键控制蛇的动作&#xff09; • 蛇撞墙死亡 • 蛇撞自身死亡 • 计算得分 • 蛇身加速、减速 • 暂停游戏 二.技术要点 C语言函数、枚举、结构体、动态内存管…

用队列实现栈——leetcode刷题

题目的要求是用两个队列实现栈&#xff0c;首先我们要考虑队列的特点&#xff1a;先入先出&#xff0c;栈的特点&#xff1a;后入先出&#xff0c;所以我们的目标就是如何让先入栈的成员后出栈&#xff0c;后入栈的成员先出栈。 因为有两个队列&#xff0c;于是我们可以这样想&…

支付宝支付流程

第一步前端&#xff1a;点击去结算&#xff0c;前端将商品的信息传递给后端&#xff0c;后端返回一个商品的订单号给到前端&#xff0c;前端将商品的订单号进行存储。 对应的前端代码&#xff1a;然后再跳转到支付页面 // 第一步 点击去结算 然后生成一个订单号 // 将选中的商…

SQL 基础 | AVG 函数的用法

在SQL中&#xff0c;AVG()是一个聚合函数&#xff0c;用来计算某个列中所有值的平均值。 它通常与GROUP BY子句一起使用&#xff0c;以便对分组后的数据进行平均值计算。 AVG()函数在需要了解数据集中某个数值列的中心趋势时非常有用。 以下是AVG()函数的一些常见用法&#xff…

DETR类型检测网络实验2---优化测试

补全reference_point Anchor-DETR提出用预定义的参考点生成query_pos; DBA-DETR提出预定义参考信息由(x,y)增至(x,y,w,h) 那么在3D检测任务中是否可以把预定义参考信息补全为(x,y,z,l,w,h,sint,cost),而query_pos都是使用xy两个维度(因为是bev网络). (这种方法在Sparse-DETR中…

CMakeLists.txt语法规则:部分常用命令说明一

一. 简介 前一篇文章简单介绍了CMakeLists.txt 简单的语法。文章如下&#xff1a; CMakeLists.txt 简单的语法介绍-CSDN博客 接下来对 CMakeLists.txt语法规则进行具体的学习。本文具体学习 CMakeLists.txt语法规则中常用的命令。 二. CMakeLists.txt语法规则&#xff1a;…

探索LLM在广告领域的应用——大语言模型的新商业模式和新个性化广告的潜力

概述 在网络搜索引擎的领域中&#xff0c;广告不仅仅是一个补充元素&#xff0c;而是构成了数字体验的核心部分。随着互联网经济的蓬勃发展&#xff0c;广告市场的规模已经达到了数万亿美元&#xff0c;并且还在持续扩张。广告的经济价值不断上升&#xff0c;它已经成为支撑大…

C++初阶之模板初阶

一、泛型编程 如何实现一个通用的交换函数呢&#xff1f; void Swap(int& left, int& right) {int temp left;left right;right temp; } void Swap(double& left, double& right) {double temp left;left right;right temp; } void Swap(char& left,…