理解Rust引用及其生命周期标识(上)

news/2025/2/28 23:39:52/文章来源:https://www.cnblogs.com/w4ngzhen/p/18744338

写在前面

作为Rust开发者,你是否还没有完全理解引用及其生命周期?是否处于教程一看就会,但在实际开发过程中不知所措?本文将由浅入深,手把手教你彻底理解Rust引用与生命周期。

关于本文的理解门槛

本文主要面向的是已经基本上了解过Rust这门语言,对引用以及生命周期(及其标识)有基本的了解,但对于包含生命周期标识的复杂场景理解吃力的Rust开发者。因此本文不会赘述讨论关于引用的语法形式,像是如果连下面的例子为什么会报错都不清楚原因的话,那么本篇就不太适合阅读了。

fn main() {let r;{let x = 5;r = &x;}println!("r: {}", r);
}

包含引用的方法

让我们从一个最简单的例子开始,假设有如下的方法签名:

fn func(num: &i32) -> &i32;

大多数的教程都会告诉你:“入参是一个引用,返回也是一个引用,在这里,返回的引用的生命周期不能超过入参引用的生命周期,... ...”。这样说确实没有错,但这句话本质上是一个结论,对于不熟悉Rust生命周期的人来说,无法清晰地理解其中的逻辑原理,是不会真正掌握这块的内容。

接下来让我们进入正题。回到上面的例子,观察这个方法签名,我们已经知道了这个方法有一个引用作为入参,且返回的也是一个引用。那它们俩有没有关联呢?答案就是:在这种场景中,即使没有生命周期标识,它俩也一定存在关系。

在讲原因前,让我们先理解一个基本的事实:引用不可能凭空产生,它一定是来源于某个实际变量。有了这个基本的事实,让我们再来分析这个方法签名。

首先入参是一个引用。考虑到“引用一定存在来源”,那么这个入参引用会来自于什么呢?很容易想到,就是调用该方法时,外部某个变量借用而来的到的引用,作为了此时的入参:

// 一些代码...
let data: i32 = 100;
// 调用方法
func(&data); // <- 方法的入参这个引用,来源于调用方法前某个变量借用而来得到的引用

入参引用我们分析好了,接下里让我们来分析返回值。返回值是一个引用,我们依然套用上面的“引用一定有其来源”来思考,这里返回的引用的来源是什么呢?首先我们考虑这里返回的引用会不会是方法中的局部变量借用而来,很显然不可能。假设代码如下:

fn func(num: &i32) -> &i32 {let some_data: i32 = 100;let some_ref: &i32 = &some_data;some_ref // 
}

在这里,我们在方法体内部创建了一个i32类型的变量,得到它的引用,再通过方法返回。然而,some_data是方法的局部变量,一旦func方法执行完毕,some_data变量对应的内存就会被释放,那么返回给外部的some_ref就成了无效的悬垂引用(Dangling References),引用着一段无效的内存。对于这种情况,Rust编译器可以非常容易的推断出你的代码语义,并禁止这种情况出现,No Way!因此,该方法返回的引用的来源就不可能是一个方法中的局部变量。

稍有经验的读者可能会想到使用'static这个特殊的生命周期标识来绕过我们的例子,但请不要着急,在本文的后面我们会提到的。当然,如果你还不太明白'static,那么太好了,可以完全忽略这段话。

既然不可能是来源于借用一个局部变量得到的结果,那么对于这个例子来说,我们就只能让其和入参进行关联了,例如编写如下的代码:

fn func(num: &i32) -> &i32 {num // <- 咱们直接把入参引用返回出去
}

虽然这个例子很简单,但是我们可以从中联想:虽然我们无法知道这个例子中func方法的具体实现,但这里方法的入参引用和出参引用之间,会因为“引用一定要有来源”这一事实,而形成一种关系:

010

也就是说,num_ref 来源于 num,而return_ref 又来源于 num_ref

020

既然存在来源关系,那么按照朴素的思维逻辑,被产出者不能存活的比被来源者还久(“我”来源于“你”,而“你”先没了,那“我”咋办 >_< )。

因此,那我们可以非常自然地给出结论:num_ref不能存活的比num久,而return_ref不能存活的比num_ref久。也就是说,对于这个方法:

fn func(num: &i32)  -> &i32;

也就是说,入参引用的要比返回的引用存活的更长才行。

值得注意的是,在本例中,目前为止,我们完全没有把Rust生命周期那套东西搬出来,仅仅是通过简单的关系逻辑梳理,就能分析出上述”生命周期“的关系。

此外,在这个场景中,我们即使不给方法上添加生命周期标识,也能通过Rust编译器的检查,毕竟,这里单个入参引用和出参引用一定有来源关系。

关于生命周期标记

其实一直以来,笔者都认为“生命周期标记” 这个命名存在一定的误导性。在笔者看来,这个东西更加适合叫做 “引用关系标记”,所以在本文,接下来内容中,笔者都将使用 “引用关系标记” 来书写表述。还是上面的例子,当我们手动加上引用关系标记以后如下所示:

fn func<'a>(num: &'a i32) -> &'a i32;

上述方法签名表达了这样一种意思:入参引用与返回的引用存在关联关系,因为它俩都用了同一个 引用关系标记(‘a) 来标识。当然,我们前面已经分析知道了,在单个引用入参,然后返回引用的场景下,入参引用与出参引用会存在关系。因此,这里的引用关系标记可以移除。

那我们可以是不是可以完全不用引用关系标记呢?让我们用一个非常经典的例子来进一步分析说明:

fn func(num_ref1: &i32, num_ref2: &i32) -> &i32;

上面的方法签名有2个输入引用和1个输出引用。同样基于 “引用一定存在来源” 的思路来分析引用,入参的num1num2和之前一样就是来自调用该方法时,外部某个变量借用而来的引用。

然而,当我们试图分析返回的引用的来源时,会发现有点困难了。返回的&i32首先不可能是方法内局部变量借用而来,所以依然与入参引用有关,那究竟是与num1有关还是与num2有关呢?回答是:没法确定。比如下面的例子:

fn func(num_ref1: &i32, num_ref2: &i32) -> &i32 {num_ref1
}

这种情况,一看就知道,返回的引用只与num1这个输入引用有关系。然而,如果这个方法的实现改为了:

/// 伪代码
fn func(num_ref1: &i32, num_ref2: &i32) -> &i32 {如果运行时,此刻的秒钟为偶数返回 num_ref1否则返回 num_ref2
}

此刻,返回引用究竟与num1有关还是与num2有关,就需要根据实际运行时情况而动态变化了,我们只能说:可能与输入引用num1有关,可能与输入引用num2有关。这时候关系图就如下所示:

030

前面也提到,A来源于B,那么A的存活不能超过B,否则,B都没了,A就没有存在的价值了。现在根据上述的关系图,在某些时候,return_ref会来源于num_ref1,因此return_ref的存活不能超过num_ref1;在另外的某些时候,return_ref会来源于num_ref2,因此return_ref的存活不能超过num_ref2。既然两种情况都会出现,同时又为了保证无论任何情况下都不会出现悬垂引用,我们能很自然的会做出这样的限定:return_ref不能超过入参num_ref1num_ref2中最短的那个引用的存活时间。

然而,当我们按照上述思路,不加任何的引用关系标记,Rust是编译不通过的。因为我们确实在实际的场景中,会存在这样的逻辑:

fn func(num_ref1: &i32, num_ref2: &i32) -> &i32 {num_ref1 // <- 确实只与num_ref1有关,跟num_ref2没有任何关系
}

你可能会觉得这可以让Rust编译器来进行分析。然而,方法逻辑的实现千千万万,Rust不能case by case的方式来理解你程序的业务逻辑,进而推断出返回的引用究竟与输入的一堆引用的中哪些有关。

因此,Rust干脆说:“嗨,引用关系你标识出来吧,我只关心引用的存活周期是否满足就好了”。也就是说,Rust编译器在处理引用安全性这方面只做好借用检查与引用生命周期的判断,至于一个方法的输入、输出的引用的关系,程序员标记好即可,这样,Rust编译器只需要关心方法签名就行。

至此,让我们再通过几个例子来巩固目前讲的内容。

示例1:输出引用与输入的n个引用都有关

fn fun1<'a>(num1: &'a i32, num2: &'a i32) -> &'a i32 {if *num1 > *num2 {num1} else {num2}
}

这种场景下,我们一般使用同一引用关系标记(这里就是'a)把它们都“关联“起来。在编译器的视角来看:”噢,这个方法返回的引用与入参的两个引用都有关系,那么作为编译器的我,要保证返回的引用存活时间不能比入参两个引用中最短的那个都存活的更长,这样才能无论哪种情况,都不会出现悬垂引用。“

示例2:输出引用只与输入的某些有关

fn fun2<'a, 'b>(num1: &'a i32, num2: &'b i32) -> &'a i32 {num1
}

在这个例子中,我们引入了两个引用关系标记('a'b),同时,返回引用标记的是'a,与输入引用中num1保持一致。那么编译器在编译过程中进行生命周期检查的时候,其视角就是:“返回引用只与num1这个引用参数存在关系,那么我接下来进行检查的时候,只需要检查一下,返回的引用存活周期不要超过num1这个引用的存活周期即可,其他就不用管了”。同时,这个例子还可以修改为:

fn fun2<'a>(num1: &'a i32, num2: &'_ i32) -> &'a i32 {num1
}

既然与第二个参数没关系,那第二个参数就写成'_吧。

特殊情况

基于前面我们提到的“引用一定有来源”,所以一般情况下这种事不会出现的:

fn fun() -> &i32;

方法没有入参,又能返回一个i32变量的引用,似乎引用的来源只能是方法中局部变量借用而来,但我们知道这是不被允许的。

但我们还可以这样编写:

const NUM: i32 = 5;fn fun() -> &'static i32 {&NUM
}fn main() {println!("num: {}", fun()); // 可以输出:“num: 5”
}

一个常量,因为其生命周期贯穿整个程序,活得最久,因此我们可以在方法中返回一个常量的引用,但需要注意的是,我们使用'static这个特殊的生命周期标记。

预告:包含引用的结构体

实际上,除开方法可能会包含输入、输出引用以外。我们还会面临包含引用的结构体。考虑到读者最好对于本文由一个消化,笔者决定将包含饮用的结构体的情况放到下一篇文章中来继续探讨。

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

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

相关文章

TCP/IP协议栈相关知识

为什么提出TCP/IP参考模型OSI参考模型比较复杂TCP和IP两大协议在网络上广泛使用 三种参考模型如今用的最多的是TCP/IP五层模型,而OSI七层模型更多用于学习。 TCP/IP常见协议要了解协议对应的传输层端口号,因为计算机是通过端口号分辨所使用的是那种协议。 应用层要了解常见协…

Vulkan环境配置 | vscode+msvc 解决方案

Vulkan环境配置 | vscode+msvc 解决方案 前言 如果作为Windows 11侠的你是一个vscode爱好者,凑巧电脑上还安装有Visual Studio,这个时候你突然想配置一个Vulkan开发环境。作为minimalist的你可不希望在电脑上安装任何额外的组件,那么这篇安装指南一定适合你。 准备工作 你需…

mysql索引原理简单说明

本次使用的是mysql5.7.17 首先说下索引中的组合索引,即多个字段组合的索引就叫做组合索引,但是什么时候会生效,什么时候会失效,我不是很清楚 首先建个表造点数据看下情况,CREATE TABLE `bank` (`id` INT(11) PRIMARY KEY AUTO_INCREMENT COMMENT 主键,`bank_end` VARCHAR…

Vulnhub-Troll-1靶机-ftp匿名登录+流量包分析+hydra爆破+ssh登录脚本提权

一、靶机搭建 选择扫描虚拟机选择路径即可二、信息收集 扫ip 靶机ip:192.168.108.144扫开放端口 开放了ftp服务扫版本服务信息信息如下 21/tcp open ftp vsftpd 3.0.2 22/tcp open ssh OpenSSH 6.6.1p1 Ubuntu 2ubuntu2 (Ubuntu Linux; protocol 2.0) 80/tcp open …

八、(了解即可)MyBatis懒加载(或者叫延迟加载)

八、(了解即可)MyBatis懒加载(或者叫延迟加载)@目录八、懒加载(了解即可)8.1 为啥需要懒加载?8.2 懒加载是什么?8.3 开启方式8.4 既然fetchType可以控制懒加载那么我仅仅配置fetchType不配置全局的可以吗?8.5 aggressiveLazyLoading是做什么么的?8.6 注意点8.7 案例验证懒加…

基于惯性加权PSO优化的目标函数最小值求解matlab仿真

1.程序功能描述基于惯性加权PSO优化的目标函数最小值求解matlab仿真。 2.测试软件版本以及运行结果展示MATLAB2022A版本运行 (完整程序运行后无水印) 3.核心程序yfits = []; % 主循环开始 for iter =1: Miteryfit = zeros(Npop,1); % 初始化函数值数组% 更新粒子速度…

题解:at_abc391_e Hierarchical Majority Vote

对于一个长度为 \(3^n\) 的 01 字符串 \(B = B_1B_2\dots B_{3^n}\),定义一种操作获得长度为 \(3^{n-1}\) 的 01 字符串 \(C = C_1C_2\dots C_{3^{n-1}}\):对于 \(i = 1,2,\dots,3^{n-1}\),令 \(C_i\) 为 \(B_{3i}\)、\(B_{3i-1}\)、\(B_{3i-2}\) 中出现次数最多的字符。现给…

解决VScode设置ctrl+,被占用

关闭启用更多系统热键设置可以用OpenArk找找

Xmx_Xms的关系与设置技巧

以下是关于JVM参数 -Xmx(最大堆内存)和 -Xms(初始堆内存)的核心关系解析与设置技巧,通过结构化表格和场景化配置示例帮助你清晰掌握调优方法:一、基础定义与关系对比表参数 作用 默认值(JDK8) 关系说明-Xms 堆内存初始分配大小 物理内存的1/64 程序启动时立即分配的内存-…

vue3创建项目时,报错crypto$2.getRandomValues is not a function

一、问题在新建项目时,使用 npm create vue@latest 创建 vue3项目后,执行命令npm install后,这时候控制台报错TypeError:crypto$2.getRandomValues is not a function二、思路起初以为依赖安装不对,使用命令rm -rf node_modules/删除依赖包,再次执行命令npm install后控制…

钉钉、飞书、企微:同样的用户场景,不一样的产品经理!

在创业团队的办公协作中,选择合适的办公工具至关重要。钉钉、飞书和企业微信作为当前主流的办公软件,各有其独特的优势和设计理念。本文从产品经理的视角出发,对比分析了这三款软件在用户体验、功能设计、收费模式以及对创业团队适配性方面的差异,供大家参考。朋友决定创业…