一、前言
如果你学过任何一门编程语言,我相信你对生命周期都应该是不陌生的,不理解的可以先看一看:常量与变量。
这个概念很通用、同时也很简单,所以这里就不过多赘述了。
生命周期带来的一个问题就是:一旦脱离了变量的生命周期,那么继续使用该变量所代表的内存就是一个危险的行为。
虽然rust通过“所有权”概念的引入,让代码变量很安全了,但要注意:rust中也是支持变量引用的。
比如:
引用写法和C++类似,都是通过&
完成,这在rust中的String与所有权中我们已经有所见识了,只不过当时用的是&str
。
上图就是一个典型的实例:变量本身的生命周期比其引用变量的生命周期要短。
变量
x
在超出本代码块后就会被清除,那么外面r
变量对其进行的引用就显然是不对的。
但此时我们可以发现,rust编译器非常的智能,可直接识别出这种错误。
而它识别的原理就是通过为所有变量添加“生命周期注解”、再通过其内部的“借用检查器”来确保引用是有效的。
大部分情况下这一过程我们是无需操心的,但在某些情况下,编译器无法判断时,就需要我们自己亲手为变量添加“生命周期注解”了。
比如下面这段代码:
该函数的作用是返回长度更长的那个字符串,此时其返回值可能是x
、也可能是y
,这取决于传入的参数。
并且由于x、y两者都是引用类型,并不拥有内存,这导致编译器在编译期间就无法确定该函数返回值的生命周期,从而出现错误。
在这种情况下,我们就需要自己手动为其添加声明周期注解了。
二、生命周期注解
在了解、学习生命周期注解之前,我们必须要明白一个事实:我们只需要为“引用变量”手动添加生命周期注解。
这意味着对于拥有内存所有权的一般变量来说,我们是不需要、也不能为其添加生命周期注解的。
因为一般变量的生命周期规则很简单,就是其所在的代码块范围内,编译器自身就完全可以胜任,而无需我们去标注。
了解了这个基本事实后,我们就可以来看看生命周期注解的原理。
以开头那段代码为例:
假设变量r
的生命周期为 'a
,那么它的长度就是从其声明的位置一直到main
后的{}
结束,也就是该代码块的结束。
假设变量x
的生命周期为'b
,那么变量x
的生命周期就是从其声明到该内部代码块结束的位置。
此时由于r
变量为x
变量的引用,但其生命周期居然比其引用的变量生命周期还长,这自然就报错了。
这便是rust编译器在编译代码时所做的事情:自动为每个变量计算生命周期,如果引用变量的生命周期比原变量生命周期还长,就报错、编译失败,从根本上杜绝不安全的行为。
但上面仅仅只是比较简单的情况,编译器能够自动为其计算生命周期,所以是不需要我们人工参与的。
而对于比较复杂的情况,比如开头提到的那个函数,编译器就无法确定了,这个时候就需要我们手动去标注生命周期注解了:
由于生命周期注解是编译器编译期间使用的,所以它用法和前面提到的泛型一样,是要通过在函数名后的<>
中进行声明,然后才能在参数中使用的。
同时要注意,生命周期注解与普通泛型参数不同之处在于,它是以符号'
开头的,并且一般都是用一个字母标识,比如这里的a
。
当然你也可以写
b
、c
、d
……,这个无所谓。
有了'a
后,我们就可以在参数中使用它来对引用变量进行标注,同时注意前面已经提到过的,它只能用于引用变量的标注,也就是写在&
符号与类型之间。
比如x: &'a str
,意思就是为引用变量x
标注其生命周期为'a
,同时参数y
、以及返回值都标注为了'a
。
标注之后我们就能发现,此时rust编译器已经不再报错了。
至于原因其实也很好理解,因为按道理来说,x
、y
这两个引用变量的声明周期是未知的、是靠外面传入的,所以它们两个的生命周期正常来说是不同的,也就是下面这样:
这导致编译器无法在编译时确定其返回值的生命周期:可能是'a
、也可能是'b
。
这种不确定的行为在rust中不被允许,所以报错了。
但此时通过我们人为的标注,让x、y这两个引用变量的生命周期一致,就成功解决了这个问题。
可这是为什么呢?毕竟我们标注的生命周期注解,实际上并不能真正决定一个变量的生命周期。