title | tags | |||
---|---|---|---|---|
Lifetime |
|
在 Rust 中,生命周期是一个非常重要的概念,用于确保引用不会悬空,即引用的数据在引用存在的时间里始终有效。Rust 编译器通过生命周期来检查这种有效性。这一节将讨论生命周期的重要性、如何使用生命周期注解,以及它如何帮助我们写出更安全的代码。
在第 5 节我们知道,Rust 中所有的对象都是有主人(owner)的,它拥有对象的所有权(ownership),所有权具有唯一性,但很多时候我们并不需要对象的所有权,有使用权就够了,而使用权在 Rust 中称之为引用(borrow),或者说借用。
let s1 = String::from("hello");
let s2 = &s1;
在上面的例子中,s1
拥有 hello
这个对象的所有权,而 s2
通过 &
修饰符表示引用这个对象,默认是不可变引用。这是一个正常的例子,但在有些情况下,引用会变得无效:
let s2;
{
let s1 = String::from("hello");
s2 = &s1;
}
println!("s2: {}", s2);
上面的例子中,s2
引用的 s1
对象的作用域在花括号内部,超出这个作用域之后,s1
这个对象会被 Rust 自动回收,此时再去打印 s2
的值就会发生错误:s1 does not live long enough, borrowed value does not live long enough
,即对象 s1
存活的时间不够长,导致 s2
引用了一个不存在的值,这种情况,我们称之为悬空引用。
Rust 中的每一个引用都有其生命周期(lifetime),也就是引用保持有效的作用域。生命周期在 Rust 中的核心作用是防止悬空引用的发生,它是许多程序错误和安全隐患的根源。生命周期确保内存安全,无需垃圾收集。
在大多数时候,我们无需手动的声明生命周期,因为编译器可以自动进行推导,但当多个生命周期存在时,编译器可能无法进行引用的生命周期分析,就需要我们手动标明不同引用之间的生命周期关系,也就是生命周期注解。
生命周期参数名称必须以撇号'
开头,其名称通常全是小写,类似于泛型其名称非常短。'a
是默认使用的名称。生命周期参数标注位于引用符 &
之后,并有一个空格来将生命周期注解与引用的类型分隔开,如 &'a i32
。
生命周期注解并不改变任何引用的生命周期的长短。它只是描述了多个引用生命周期相互的关系,便于编译器进行引用的分析,但不影响其生命周期。
生命周期即 Rust 中值的生老病死,而生命周期注解就是用于约定多个引用之间生死的关系,就像桃园三结义中誓约的那样:不求同年同月同日生,但求同年同月同日死,这三兄弟之间的誓约就如同 Rust 中的生命周期注解。誓约是为了防止有人背信弃义,而 Rust 生命周期注解是为了编译器进行分析,防止出现悬垂引用。有了誓约并不代表大家就一定一起赴死,就如同生命周期注解并不改变值的生命周期一样。
fn borrow<'a>(x: &'a i32, y: &'a i32) -> &'a i32 {
if x > y { x } else { y }
}
这个函数 borrow
接收两个引用参数,并返回一个它们中的一个。生命周期注解 'a
指示这两个输入引用和返回引用必须拥有相同的生命周期。
生命周期在结构体定义中尤其重要,尤其是当结构体要包含对某种数据的引用时。
struct Book<'a> {
title: &'a str,
pages: i32,
}
fn main() {
let title = String::from("Rust Programming");
let book = Book {
title: &title,
pages: 384,
};
println!("Book: {} - {} pages", book.title, book.pages);
}
在这个例子中,Book
结构体有一个生命周期注解 'a
,这意味着字段 title
的生命周期至少要和 Book
实例一样长,否则就会发生悬空引用。
在某些常见情况下,Rust 允许省略生命周期注解。编译器遵循一组特定的规则(称为生命周期省略规则),在这些规则适用的情况下,可以推断出引用的生命周期。
- 每个引用参数都有自己的生命周期参数。
- 如果只有一个输入生命周期参数,该生命周期被赋给所有输出生命周期参数。
- 如果有多个输入生命周期参数,但其中之一是
&self
或&mut self
(说明是方法),则self
的生命周期被赋给所有输出生命周期参数。
考虑到更复杂的场景,显式生命周期注解变得尤为重要。它能确保代码在引用和数据管理方面的正确性,特别是在多个不同生命周期和复杂数据类型交互时。
fn longest<'a, 'b: 'a>(x: &'a str, y: &'b str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
这个函数 longest
涉及到了两个不同的生命周期注解 'a
和 'b
,而 'b: 'a
表示输入参数 y
的生命周期至少与输入参数 x
相同,或比它更长。函数返回值中 'a
表示返回的引用将具有与输入参数 x
相同的生命周期,也就是生命周期中最小的那个。这样可能比较抽象,我们看下具体的例子:
fn main() {
let string1 = String::from("abcdefghijklmnopqrstuvwxyz");
{
let string2 = String::from("123456789");
let result = longest(string1.as_str(), string2.as_str());
println!("The longest string is {}", result);
}
}
上面的代码展示了 string1
和 string2
这两个不同生命周期的变量,前者的生命周期位于外部的 {}
中,而后者的生命周期位于内部的 {}
中,所以 'a
代表的生命周期范围是两者中较小的那个,即内部的 {}
,此时返回值 result
的生命周期也是属于内部的 {}
,即返回值能够保证在 string1
和 string2
中较短的那个生命周期结束前有效,此时不会发生悬垂引用,编译通过。
接下来,让我们尝试另外一个例子,该例子揭示了 result
的引用的生命周期必须是两个参数中较短的那个。
fn main() {
let string1 = String::from("abcdefghijklmnopqrstuvwxyz");
let result;
{
let string2 = String::from("123456789");
result = longest(string1.as_str(), string2.as_str());
}
println!("The longest string is {}", result);
}
此时 'a
代表的生命周期范围依旧是变量 string1
和 string2
中最小的那个,即内部的 {}
,但返回值 result
的生命周期范围却是外部的 {}
,而不是内部的 {}
,也就意味着 result
可能会引用一个无效的值,因此编译失败。
注意:通过人为观察
result
的引用应该为string1
,这样返回值result
和string1
的作用域是一致的,理论上是应该编译通过的。但是,Rust 的编译器会采用保守的策略,我们通过生命周期标注告诉 Rust,longest
函数返回值的生命周期是传入参数中较小的那个变量的生命周期,因此 Rust 编译器不允许上述代码通过,因为可能存在无效引用。
理解和正确使用生命周期是掌握 Rust 的重要部分。生命周期注解帮助 Rust 编译器保证引用的有效性,从而让你的程序在处理引用时更加安全。虽然开始时可能会觉得生命周期有些复杂,但随着实践的深入,你会逐渐领会它们的重要性和用法。掌握生命周期让你能写出更健壮、安全的 Rust 代码。如果你有任何问题或需要更多例子,请随时提问!