Rust生命周期的理解
前言
这篇文章的目的是让读者最快最直观的了解什么是生命周期,以及为什么有生命周期,为了达到这个目的——即降低复杂性,本篇文章的用词可能不够严谨,见谅。
引用和所有者
所有者
为了保证一个值会在它作用域结束时被销毁,Rust引入了所有权机制。
fn noname() {
let a = String::from("ABCDEFG");
b = a;
}
上面的代码会发生什么?
let a = String::from("ABCDEFG");
将后面的String
值给到变量a
,现在,可以说这个a
就是字符串值的所有者b = a;
,将a
中的字符串值的所有权转移给b
,现在,b
是字符串值的所有者,a
失效了。
所有者负责该变量的销毁,在Rust中,当所有者所在的作用域结束,其值就会被自动销毁,所以:
fn noname() {
let a = String::from("ABCDEFG");
b = a;
} // b的值在这里被销毁,a由于已经不拥有值,所以无需处理
但大多数时候,你都希望只是简单的访问或者修改下值,而不是拿到它的所有权,比如,你不想在方法调用时把一个变量传进去,然后它就不能用了:
fn func(string: String) { // 参数值的所有权被转移给变量string
// do something
} // string脱离作用域,值被销毁
fn main() {
let a = String::from("ABCDEFG");
func(a); // a对其值的所有权转移到函数中,a不再拥有任何值
println!("{}", a); // 访问没有值的变量,无法通过编译
}
这个时候,你需要用到引用。
引用
fn func(string: &String) { // string是一个String类型的引用,并不拥有其值
// do something
} // string引用被销毁,不影响值
fn main() {
let a = String::from("ABCDEFG");
func(&a); // 产生a的一个引用
println!("{}", a); // 正常编译!
}
我们在这篇文章里不涉及引用到底是什么样的一个东西,它和指针有什么区别,引用在堆上还是栈上等问题,这些问题,你可以去看《Rust In Action》,你可以暂时把它想成一个指向原值的指针,但这个指针不同于C的指针,引用有额外的安全机制,即Rust会保证引用永远不会指向失效的值。
引用的生命周期
引用有额外的安全机制,即Rust会保证引用永远不会指向失效的值。
问题就出在这句话上,有了这句话,才有了生命周期这个小碧7。还是刚刚的例子:
fn func(string: &String) {
// Rust保证引用不会指向失效的值,所以在该方法中你可以放心大胆的访问string而不会出现任何内存错误
// 也就是说,string的原值在方法调用期间必须一直是有效的
}
为了满足上面的保证,Rust必须确保,传入func
的引用,其原值的存活时间大于等于func
的作用域,只有这样,引用才不会指向失效的值(实际上作为参考的并不是func
的作用域,不过在这个例子上先这样理解):
我们可以把这个存活时间理解为生命周期。
当引用被创建,一个引用的生命周期开始,当引用结束最后一次使用,该引用的生命周期结束。更复杂一点的话,引用的生命周期可以是分离开的多个区域,这通常在有流程控制结构时发生,在这篇文章里我们不会触碰这些复杂的结构,可以在《Rust for Rustaceans》一书的第一章中找到。
引用的生命周期,必须小于等于其值的生命周期。
我来举一个反例:
fn invaild_ret_ref() -> &String {
&String::from("Hello")
}
这个函数返回一个String
的引用,它在函数中使用String::from
创建了一个String
值,然后试图将它的引用返回,可是在invaild_ret_ref
函数结束后,这个String
值就已经被销毁了,该引用不可能能够指向有效的数据,Rust编译器会拒绝这个代码。
我很喜欢张汉东老师对《Rust for Rustaceans》的翻译样章中把生命周期(lifetime)翻译成生存期,生存期确实比生命周期更加生动,但为了和已有的术语对应,这里我还是使用生命周期。
生命周期标记
正如我们看到的,Rust的引用代表对值的一次借用,它们有着种种限制,所以,在函数中、在结构体中、在方法等等位置上使用引用时,你都要给Rust编译器一些关于引用的提示,这种提示,就是生命周期标记。
考虑下面的例子:
fn longer_one(a: &String, b: &String) -> &String {
if a.len() > b.len() {
a
} else {
b
}
}
我不是要故意把例子写的复杂,因为对于简单的情况,Rust编译器已经足够聪明到让你能够省略生命周期标识符,稍后我们会谈到。
这个函数接收两个String
引用,返回其中较长的那个引用。首先先说结论,它无法通过编译。
为什么呢?考虑下面的调用:
fn main() {
let str_1 = String::from("Hello");
let longer: &String;
{
let str_2 = String::from("A");
longer = longer_one(&str_1, &str_2);
} // str_2的值被销毁
println!("{}", longer);
} // str_1的值被销毁
这个调用有什么问题呢?str_1
的值Hello
,它的生命周期是整个main
函数,直到main
结束,它才会被销毁,而str_2
的值A
,它在它所在的代码块结束后就被销毁。所以,它们的引用有效的生命周期范围也是不同的,str_2
的引用的生命周期就是中间的那个只有两行代码的代码块。
引用类型变量longer
接收longer_one
函数的返回结果,该函数可能返回str_1
的引用和str_2
的引用其中之一,如果恰好str_1
比较长,那么longer
变量就是str_1
的引用,我们可以在后面的println
语句中访问它,因为它仍处在自己的生命周期中,而如果是str_2
较长,我们稍后如果访问它就是在访问一个无效的内存值,这是很危险的。
也就是说,在这种情况下,Rust编译器无法确定哪个引用是被返回的,如果Rust放任这种情况,让这种模棱两可的代码通过编译,那么你的程序偶尔会正常运行,偶尔会出现内存错误。
如果你已经感到晕乎乎的了,不妨休息一下。
如果只返回a
呢?
上面的代码从本质上就有问题,无论你使用还是不用生命周期标记,都无法解决这个问题,生命周期标记也不是为了处理这个问题而存在的。不过,我们考虑下面的代码:
fn longer_one(a: &String, b: &String) -> &String {
a
}
现在,该方法被修改了,它只会返回第一个引用,也就是a
,这次,按理说上面的调用不会出现问题了,因为str_2
的引用将永远不可能被赋值给longer
,后面的访问也没有风险。
但是Rust编译器无法识别这种情况,该函数还是无法通过编译,而生命周期标记,本质目的就是为了在你能够确保你的代码正常的情况下,提示下Rust编译器,我不会出错,放我过去吧。
给编译器关于引用生命周期的提示
Rust的生命周期标识是通过类似泛型的语法来定义的,不得不说,看起来很丑,且没有道理:
fn longer_one<'a>(a: &'a String, b: &String) -> &'a String {
a
}
在这里,我们定义了一个生命周期标记'a
,生命周期必须以引号开头。我们告诉编译器,参数a
这个引用的生命周期是'a
,并且我返回值的生命周期和它的生命周期相同,也是'a
。
此时,你再进行编译,代码已经可以通过编译了,外面的代码清楚的知道在第二个参数位置上的str_2
是不会被返回并赋值给longer
的,所以,编译器可以放过我们的代码。
关于为什么使用泛型语法,下面是我自己的猜测:
考虑这样的函数签名:
fn test<T>(a: T) -> T
,泛型允许我们传入任何类型,T
可以是任意类型,但是,最主要的,泛型约束了该函数的参数a
和返回值的类型必须是相同的。换句话说,当a
的类型确定了,返回值类型就确定了。回想刚刚的生命周期泛型标记,
'a
可以是任意生命周期(实际上仍有一些限制,我们稍后会说),但是,泛型生命周期约束了该函数的返回值必须和a
的生命周期一致,当a
的生命周期确定了,返回值的生命周期就确定了。你可以先按上面的理解,但在生命周期中,事情会稍微有些不一样,我们马上会看到。
和泛型差在哪?
我们可以使用比要求更大的生命周期
为了方便演示,且不引入&str
类型(因为对于初学者来说,String
可能会和&str
混淆),我们把例子换成i32
类型,还是之前一样的代码:
fn first<'a>(a: &'a i32, b: &i32) -> &'a i32 {
a
}
我应该不用再解释该代码的意思以及为什么它能够通过编译,而去掉了生命周期标记'a
就不能了,你可以自己试着讲下。
现在,我们看一个神奇的东西:
static GLOBAL: &i32 = &6i32;
fn first<'a>(a: &'a i32, b: &i32) -> &'a i32 {
GLOBAL
}
该代码的返回值并不和参数'a
具有相同生命周期,但是该代码也能通过编译。为什么呢?我们上面的解释站不住脚了吗?
确实,站不住脚了!!!!上面,为了让你理解为什么Rust使用泛型语法来标记生命周期描述符,我给你讲了一些并非事实的东西,虽然无伤大雅。
在泛型中:
fn test<T>(a: T) -> T {
// dosomething
a
}
当参数a
传入,它的类型被确定,T
就等于这个类型了,你不可以再使用一个其它类型来代表它。
而在泛型生命周期上:
static GLOBAL: &i32 = &6i32;
fn first<'a>(a: &'a i32, b: &i32) -> &'a i32 {
GLOBAL
}
泛型生命周期标记'a
不是这样,当你传入a
,你可以在需要a
的生命周期时使用大于等于参数a
的生命周期,而并非必须和它一致。
回到上面的代码,GLOBAL
是一个static
变量,它的值是一个引用类型,static
变量的生命周期是'static
,它是贯穿整个程序始终的。也许后面我们会详细介绍它。
如果考虑类型兼容性,如继承和实现关系,其实泛型也有这种特性,当然Rust里没有。
这一特性也称作生命周期的强制转换,即一个较长的生命周期可以转换成一个较短的生命周期以进行使用。见:https://www.yuanmadesign.com/rustexample/scope/lifetime/lifetime_coercion.html
插曲:防止你理解歪了因为我就理解歪了
虽然你可以在需要一个生命周期时,使用另一个更大的生命周期去代表它,但是,从上面代码的角度说,外界并不知道你返回的引用具有'static
生命周期,它依然会认为,你返回的是第一个参数的生命周期。
static GLOBAL: &i32 = &6i32;
fn first<'a>(a: &'a i32, b: &i32) -> &'a i32 {
GLOBAL
}
你在这样使用它时,它还会拒绝你的代码:
let ret: &i32;
{
let a: i32 = 12;
let b: i32 = 14;
ret = first(&a, &b); // 已经被借用的a的值的存活时间不够
} // a在这里被销毁,但它仍被借用
println!("{}", ret); // 它会在这里被借用
代表更小的一个生命周期
下面的代码的生命周期如何解释?:
fn longer_one<'a>(a: &'a String, b: &'a String) -> &'a String {
if a.len() > b.len() {
a
} else {
b
}
}
参数a
和b
都具有生命周期'a
,并且返回值也具有生命周期'a
,如果参数a
和参数b
这两个引用的生命周期一致,显然就无需解释了,'a
就是它们两个的生命周期,可万一它们的生命周期不一致呢?
假如a
的更大,b
的更小,那么很自然的,返回值代表更小的那个生命周期,外部会将返回值的生命周期当作a
和b
中更小的那个来用。
生命周期标记作用于多个引用时,其代表最小的那个引用的生命周期
总结
- 生命周期是一个引用的生存期,在代码编译时,Rust会确保引用的生命周期不会超过其值的有效范围
- 在Rust编译器感到迷惑的时候,你得给它一些有关生命周期的提示,这种提示叫做生命周期标注
- 生命周期标注使用泛型语法声明,比如
<'a>
- 在引用上使用生命周期标注的语法是
&'a Type
- 生命周期和泛型不一样的地方在于,在需要一个生命周期的位置,你可以使用一个更大的生命周期代表,这称作大生命周期到小生命周期的强制转换。在泛型生命周期应用于多个引用上时,其取最小的那个引用的生命周期。
如果你已经感到晕乎乎的了,不妨休息一下。
在结构体中使用生命周期标注
你可以在结构体中使用生命周期标注
#[derive(Debug)]
struct NamedBorrowed<'a> {
x: &'a i32,
y: &'a i32,
}
如果你已经理解了上面所有内容,把它用在结构体里也没什么稀奇的。
这个生命周期标注约束了几点:
- 两个引用的生命周期必须大于等于结构体的生命周期,否则就有可能出现内存错误
- 生命周期标记
'a
,代表a和b中更小的那个,那么该结构体的生命周期也必须小于等于x
、y
两个引用的生命周期里最小的那个
fn main() {
let a: i32 = 1;
let my_struct: NamedBorrowed;
{
let b: i32 = 4;
my_struct = NamedBorrowed {
x: &a,
y: &b
}
}
}
结构体my_struct
,只能用于main
其中b
被定义的代码块中。
trait中使用生命周期标注
// 带有生命周期标注的结构体。
#[derive(Debug)]
struct Borrowed<'a> {
x: &'a i32,
}
// 给 impl 标注生命周期。
impl<'a> Default for Borrowed<'a> {
fn default() -> Self {
Self {
x: &10,
}
}
}
在这里,Borrowed
结构体需要一个引用,该引用的生命周期必须比结构体存活时间长,即结构体不能比'a
所代表的生命周期活得更久。
你为它实现了Default
trait,并且在其方法中,你为其中的字段x
设置了一个引用值,你使用了一个'static
生命周期的常量,它显然符合'a
的要求。
和泛型共同使用
struct Ref<'a, T: 'a>(&'a T);
T: 'a
,代表泛型中所有引用的生命周期至少为'a
,而Ref
中存储了T
的引用,所以它也被限制了,它的生命周期不能比'a
更长。
T: Trait + 'a
代表T
类型必须实现Trait
trait,并且在T
中的所有引用都必须比'a
活得更长。
生命周期默认省略规则
每一个是引用的参数都有它自己的生命周期参数
fn func(string: &String) {}
这个函数会被Rust编译器认为是
fn func<'a>(string: &'a String) {}
而
fn func(string: &String, string2: &String) {}
会被认为:
fn func<'a, 'b>(string: &'a String, string2: &'b String) {}
依此类推
若你的需求恰好如此,就不用手动声明生命周期
如果只有一个输入生命周期参数,那么它被赋予所有输出生命周期参数
即,下面两个函数
fn func<'a>(string: &'a String) -> &String {}
fn func(string: &String) -> &String {}
会被认为是
fn func<'a>(string: &'a String) -> &'a String {}
上面第二个先应用规则1,再应用规则2
方法的输出生命周期是self的生命周期
Rust中,方法是带有&self
或&mut self
参数的函数,其输出生命周期被设置成self
(结构体本身)的生命周期