Rust生命周期
程序中每个变量都有一个固定的作用域,当超出变量的作用域以后,变量就会被销毁。变量在作用域中从初始化到销毁的整个过程称之为生命周期。
rust的每个函数都会有一个作用域,也可以在函数中使用一对花括号再内嵌一个作用域。比如如下代码中就在main函数的函数作用域中又内嵌了一个作用域:
1 | fn main() { |
上面代码存在两个作用域,一个是main
函数本身的作用域,另外一个是在main
函数中使用一对{}
定义了一个内部作用域。第2行代码声明了变量a
,它的作用域是整个main
函数,也可以说它的生命周期是从第2行代码到第6行代码。在第4行代码中声明了变量b
,它的作用域是第4行到第6行。我们可以发现变量的生命周期是有长短的。
生命周期与借用
rust中的借用是指对一块内存空间的引用。rust有一条借用规则是借用方的生命周期不能比出借方的生命周期还要长。
例如:
1 | fn main() { |
上面第5行代码把变量b
借给了变量a
,所以a是借用方,b是出借方。可以发现变量a
(借用方)的生命周期比变量b
(出借方)的生命周期长,于是这样做违背了rust的借用规则(借用方的生命周期不能比出借方的生命周期还要长)。因为当b在生命周期结束时,a还是保持了对b的借用,就会导致a所指向的那块内存空间已经被释放了,那么变量a就会是一个悬垂引用。
运行上面代码会报如下错误:
1 | error[E0597]: `b` does not live long enough |
意思就是说变量b的生命周期不够长。变量b已经被销毁了仍然对它进行了借用。
一个正确的例子:
1 | fn main() { |
观察上面代码发现变量b(借用方)的生命周期要比变量a(出借方)的生命周期要短,所以借用检查器会通过。
函数中的生命周期参数
对于一个参数和返回值都包含引用的函数而言,该函数的参数是出借方,函数返回值所绑定到的那个变量就是借用方。所以这种函数也需要满足借用规则(借用方的生命周期不能比出借方的生命周期还要长)。那么就需要对函数返回值的生命周期进行标注,告知编译器函数返回值的生命周期信息。
我们下面定义一个函数,该函数接收两个i32
的引用类型,返回大的那个数的引用。
示例:
1 | fn max_num(x: &i32, y: &i32) -> &i32 { |
由于缺少生命周期参数,编译器不知道max_num
函数返回的引用生命周期是什么,所以运行报错:
1 | error[E0106]: missing lifetime specifier |
函数的生命周期参数声明在函数名后的尖括号<>
里,然后每个参数名跟在一个单引号'
后面,多个参数用逗号隔开。如果在参数和返回值的地方需要使用生命周期进行标注时,只需要在&
符号后面加上一个单引号'
和之前声明的参数名即可。生命周期参数名可以是任意合法的名称。例如:
1 | fn max_num<'a>(x: &'a i32, y: &'a i32) -> &'a i32 { |
上面代码对函数max_num
的参数和返回值的生命周期进行了标注,用于告诉编译器函数参数和函数返回值的生命周期一样长。在第13行代码对max_num
进行调用时,编译器会把变量x的生命周期和变量y的生命周期与max_num
函数的生命周期参数'a
建立关联。这里值得注意的是,变量x和变量y的生命周期长短其实是不一样的,那么关联到max_num函数的生命周期参数'a
的长度是多少呢?实际上编译器会取变量x的生命周期和变量y的生命周期重叠
的部分,也就是取最短的那个变量的生命周期与'a
建立关联。这里最短的生命周期是变量y
,所以'a
关联的生命周期就是变量y的生命周期。
运行上面代码,会有报错信息:
1 | error[E0597]: `y` does not live long enough |
报错信息说变量y的生命周期不够长,当y的生命周期结束后,仍然被借用。
我们仔细观察发现max_num
函数返回值所绑定到的那个变量max
(借用方)的生命周期是从第10行代码到第16行代码,而max_num
函数的返回值(出借方)的生命周期是'a
,'a
的生命周期又是变量x的生命周期和变量y的生命周期中最短的那个,也就是变量y的生命周期。变量y的生命周期是代码的第12行到第14行。所以这里不满足借用规则(借用方的生命周期不能比出借方的生命周期还要长)。也就是为什么编译器会说变量y的生命周期不够长的原因了。函数的生命周期参数并不会改变生命周期的长短,只是用于编译来判断是否满足借用规则。
将代码做如下调整,使其变量max的生命周期小于变量y的生命周期,编译器就可以正常通过:
1 | fn max_num<'a>(x: &'a i32, y: &'a i32) -> &'a i32 { |
如果函数参数的生命周期参数与函数返回值的生命周期参数不建关联的话,那么生命周期参数就没有任何意义。
例如:
1 | fn max_num<'a, 'b>(x: &'a i32, y: &'a i32) -> &'b i32 { |
上面代码中函数返回值的生命周期参数'b'
是未知的,编译器不知道这个'b'
的具体生命周期是多少,所以没有任何意义。
可以显示指明多个生命周期参数间的关系,从而使每个生命周期参数都有一个具体的的生命周期。例如:
1 | fn max_num<'a, 'b: 'a>(x: &'a i32, y: &'b i32) -> &'a i32 { |
上面代码使用'b: 'a
来标注'a
与'b
之间的生命周期关系,它表示'a
的生命周期不能超过'b
,即函数返回值的生命周期'a
(借用方)不能超过'b``(出借方),
‘a也不会超过
‘a`(出借方)。
结构体中的生命周期参数
一个包含
引用成员
的结构体,必须保证结构体本身的生命周期不能超过任何一个引用成员
的生命周期。否则就会出现成员已经被销毁之后,结构体还保持对那个成员的引用就会产生悬垂引用。所以这依旧是rust的借用规则即借用方(结构体本身)的生命周期不能比出借方(结构体中的引用成员)的生命周期还要长。因此就需要在声明结构体的同时也声明生命周期参数,同时对结构体的引用成员进行生命周期参数标注。
结构体生命周期参数声明在结构体名称后的尖括号<>
里,每个参数名跟在一个单引号'
后面,多个参数用逗号隔开。在进行标注时,只需要在引用成员的&
符号后面加上一个单引号'
和之前声明的参数名即可。生命周期参数名可以是任意合法的名称。例如:
1 | struct Foo<'a> { |
上面代码可以把结构体Foo
的生命周期与成员v
的生命周期建立一个关联用于编译器进行借用规则判断。
下面是一个违反借用规则的例子:
1 |
|
上面代码的第14行到15行foo
的生命周期依然没有结束,但是它所引用的变量v
已经被销毁了,因此出现了悬垂引用。编译器会给出报错提示:变量v
的的生命周期不够长。
静态生命周期参数
有一个特殊的生命周期参数叫
static
,它的生命周期是整个应用程序。跟其他生命周期参数不同的是,它是表示一个具体的生命周期长度,而不是泛指。static
生命周期的变量存储在静态段中。
所有的字符串字面值都是 'static
生命周期,例如:
1 | let s: &'static str = "codercat is a static lifetime."; |
上面代码中的生命周期参数可以省略,就变成如下形式:
1 | let s: &str = "codercat is a static lifetime."; |
还有static
变量的生命周期也是'static
。
例如:
1 | static V: i32 = 123; |
可以对函数的参数进行限制,让其只能只能接受静态变量:
1 | fn max_num(x: &'static i32, y: &'static i32) -> &'static i32 { |
在使用static
生命周期参数的时候,由于它已经内置在编译器中了,所以不需要进行声明。在结构体中的使用方式也类似就不再次举例了。
下面举一个特殊的例子:
1 | fn max_num<'a>(x: &'a i32, y: &'a i32) -> &'a i32 { |
还是之前的max_num
函数。在代码的第12行定义了一个静态变量,它的生命周期是'static
。max_num
函数的生命周期参数'a
会取变量x
的生命周期和变量Y
的生命周期重叠的部分。所以传入max_num
函数并不会报错。
总结
以上内容是我个人在学习rust生命周期参数相关内容时的总结,如有错误欢迎指正。文中的借用和引用实际上是一个东西。