理解Rust引用及其生命周期标识(上)
写在前面
作为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方法的具体实现,但这里方法的入参引用和出参引用之间,会因为“引用一定要有来源”这一事实,而形成一种关系:

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

既然存在来源关系,那么按照朴素的思维逻辑,被产出者不能存活的比被来源者还久(“我”来源于“你”,而“你”先没了,那“我”咋办 >_< )。
因此,那我们可以非常自然地给出结论: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个输出引用。同样基于 “引用一定存在来源” 的思路来分析引用,入参的num1、num2和之前一样就是来自调用该方法时,外部某个变量借用而来的引用。
然而,当我们试图分析返回的引用的来源时,会发现有点困难了。返回的&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有关。这时候关系图就如下所示:

前面也提到,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_ref1和num_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这个特殊的生命周期标记。
预告:包含引用的结构体
实际上,除开方法可能会包含输入、输出引用以外。我们还会面临包含引用的结构体。考虑到读者最好对于本文由一个消化,笔者决定将包含饮用的结构体的情况放到下一篇文章中来继续探讨。
理解Rust引用及其生命周期标识(上)的更多相关文章
- 【译】深入理解Rust中的生命周期
原文标题:Understanding Rust Lifetimes 原文链接:https://medium.com/nearprotocol/understanding-rust-lifetimes- ...
- 我所理解的 Laravel 请求 生命周期
转载自:https://laravel-china.org/topics/3343/my-understanding-of-the-laravel-request-life-cycle 当你使用一个工 ...
- MVVM的理解和Vue的生命周期
一.对于MVVM的理解? MVVM 是 Model-View-ViewModel 的缩写.Model代表数据模型,也可以在Model中定义数据修改和操作的业务逻辑.View 代表UI 组件,它负责将数 ...
- Spring官网阅读(九)Spring中Bean的生命周期(上)
文章目录 生命周期回调 1.Bean初始化回调 2.Bean销毁回调 3.配置默认的初始化及销毁方法 4.执行顺序 5.容器启动或停止回调 Lifecycle 接口 LifecycleProcesso ...
- asp.net MVC 应用程序的生命周期(上)
首先我们知道http是一种无状态的请求,他的生命周期就是从客户端浏览器发出请求开始,到得到响应结束.那么MVC应用程序从发出请求到获得响应,都做了些什么呢? 本文我们会详细讨论MVC应用程序一个请求的 ...
- 理解React组件的生命周期
本文作者写作的时间较早,所以里面会出现很多的旧版ES5的时代的方法.不过,虽然如此并不影响读者理解组件的生命周期.反而是作者分为几种不同的触发机制来解释生命周期的各个方法,让读者更加容易理解涉及到的概 ...
- [Java]类的生命周期(上)类的加载和连接[转]
本文来自:曹胜欢博客专栏.转载请注明出处:http://blog.csdn.net/csh624366188 类加载器,顾名思义,类加载器(class loader)用来加载 Java 类到 Java ...
- JVM详解之:汇编角度理解本地变量的生命周期
目录 简介 本地变量的生命周期 举例说明 优化的原因 总结 简介 java方法中定义的变量,它的生命周期是什么样的呢?是不是一定要等到方法结束,这个创建的对象才会被回收呢? 带着这个问题我们来看一下今 ...
- Rust 闭包与生命周期
- Rust生命周期bound用于泛型的引用
在实际编程中,可能会出现泛型引用这种情况,我们会编写如下的代码: struct Inner<'a, T> { data: &'a T, } 会产生编译错误: error[E0309 ...
随机推荐
- 2.mysql授权认证
权限系统介绍 ● 什么是权限系统 权限系统是授予来自某个主机的某个用户可以查询.插入.修改.删除等数据库操作的权限 不能明确的指定拒接某个用户的连接 权限控制(授权与收回)的执行语句包括 create ...
- 中电金信:四川农担X中电金信大数据智能风控平台 护航金融服务乡村振兴
高质量金融服务是乡村振兴的重要支撑.四川省农业融资担保有限公司(以下简称"四川农担")持续探索融资担保服务,努力满足"三农"领域多样化.多层次融资担保需求的同 ...
- 用Python让两组数据纵向排序
一.引言 在数据处理和分析中,排序是一项非常基础且重要的操作.排序可以帮助我们更好地理解数据,发现数据中的模式和规律.在Python中,我们可以使用多种方法对数据进行排序.本文将详细介绍如何使用Pyt ...
- 龙哥量化:注册simnow上期所的期货仿真模拟交易账户教程步骤
永远顺着趋势交易 在技术分析这种市场研究方法中,趋势的概念绝对是核心内容.分析师所使用的全部工具, 诸如支撑和阻挡水平.价格形态.移动平均线.趋势线等等,其唯一的目的就是辅助我们估量市场趋势, 从而顺 ...
- IM开发者的零基础通信技术入门(十二):上网卡顿?网络掉线?一文即懂!
[来源申明]本文引用了微信公众号"鲜枣课堂"的<上网慢?经常掉线?这篇文章告诉你该怎么办!>文章内容.为了更好的内容呈现,即时通讯网在引用和收录时内容有改动,转载时请注 ...
- Abp vNext 扩展属性
扩展属性 我们发现abp的默认都会有一个ExtraProperties属性,那么他的作用是什么呢?当我们使用abp的时候,需要对其原有的表进行扩展或者添加我们想要的字段,又不想改源码,以最小的方式实现 ...
- .NET Core GC对象 分配(GC Alloc)底层原理浅谈
对象分配策略 .NET程序的对象是由CLR控制并分配在托管堆中,如果是你,会如何设计一个内存分配策略呢? 按需分配,要多少分配多少,移动alloc_ptr指针即可,没有任何浪费.缺点是每次都要向OS申 ...
- 记录uniapp上传图片转base64
// 图片转base64 imageToBase64() { return new Promise((reslove, reject) => { uni.getFileSystemManager ...
- 敏捷开发:如何高效开每日站会(Daily Stand-up Meeting)
介绍 在敏捷开发框架 Scrum 中,每日站会(Daily Stand-up Meeting,又叫 Daily Scrum)是 Sprint 迭代开发中,一个很重要的流程,一个重要的例会.在有限的时间 ...
- Appium_iOS_Safari测试脚本(2)
经过多次调试,在Safari上的测试脚本终于可以运行了,不过部分元素还是无法识别,还需要继续调试: #!/usr/bin/env/python # -*-coding:utf-8-*- import ...