背景
与所有权类似,引用也有在代码中生效的范围,超出了这个范围,该引用就会被无效化,引用只能够在这个范围内被访问。而这个范围就被称为作用域,也称为生命周期。
在很多语言中,生命周期并不是一件很值得讨论的事情,因为逃逸机制的存在,变量可以隐式地无限拓展自己的生命周期,因为GC变量也可以被很好地回收。而Rust选择需要我们精准地管理起不同变量,不同引用的生命周期,以达到安全,稳定使用的效果,使其绝对不会出现悬垂指针或悬垂引用。所以,在Rust中,引用的生命周期是一件值得讨论的事情,并且在开发中会时刻被编译器和脑子提醒。
Rust没有空指针异常,他将空指针的处理从运行时“移动”到了编译期。
当然,单一引用的生命周期一般来说是明确的,从变量的声明到大括号的结束,或者在某个函数的出口作为生命周期的终结,都可以被借用检查器(borrow checker)轻松地自动检查并处理。
生命周期的不明确
与清晰相反的是模糊,规则的光芒不可能明确地照耀每一行代码。当引用的生命周期没有确定的结束时间时,往往就会让检查器也开始迷惑。这时候需要程序员手动指定生命周期,以让检查器能够确定在什么时间,这个引用仍然是有效的。
我们可以看到以下代码:
在代码中,x和y都为&str引用,并且返回较长的一个引用。那么在Rust中,返回给外部的引用,他的生命周期会更长,而没有返回的引用的生命周期就到此为止。但是问题在于,长度是运行时确定的,编译时没有办法确定该返回哪个变量。这个时候检查器对于不确定的引用生命周期就犯迷糊了,所以这就是少数的需要我们手动指定生命周期的情况。
手动指定生命周期
如上文所说,我们需要通过手动编写“约定”的方式,告诉编译器引用之间生命周期的关系。在新的代码中,我们加入了
'a 作为生命周期的注解,这是一个特殊的语法,以`开始,后面跟着一个标识符,一般用a b c d这样的单字符作为不同生命周期的区分。在函数签名中,我们在每个引用前面都加上了同样标识的生命周期注解,这告诉编译器:longest函数返回值的引用的生命周期应该与传入参数中生命周期较短的引用一致。
非常需要注意的是:手动指定生命周期本质上指定的是入参和出参之间的关联关系,或者说出参的生命周期相对于入参的生命周期的关系,他们关系是同时存在的,指定的时候也需要同时指定。
在结构体中加入声明周期
在结构体中,如果其中有某个成员变量是另一个结构体的引用,那么我们同样也要手动指定他的生命周期。在早期的编译器中,生命周期的添加是自动被完成的,但是随着语言的发展,隐式推测成员变量生命周期变得不太可靠。所以在如今,我们需要手动指定引用成员变量的生命周期,在这里,语义变成了:config引用的生命周期应当与App本身的生命周期一致。
全局变量
在程序中,我们不难遇到需要全局变量的情况。这时候手动声明变量的声明周期就是一件必要的事情。一般来说,字符串字面量是天然全局的,我们同样也可以手动指定。
结论
生命周期总体来说是一个在编译期发生的事情,而不是运行期。它迫使我们需要认真地想清楚,每个参数的生命周期与哪些其他参数相关联,他们之间的关联关系是如何。但是不用太担心,编译器非常智能,它不仅为我们完成了大部分生命周期的推断,还会在需要手动添加生命周期的地方给予我们足够的提示。