生命周期是什么?
生命周期的定义
在 Rust 中,每个引用都有一个生命周期,表示引用值在内存中存在的时间。生命周期确保引用在其整个生命周期内保持有效。它们的存在是为了保证引用的有效性。
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
在上面的代码中,函数 longest 有两个参数,它们都是字符串切片的引用,并且有一个返回值。由于 Rust 高度关注内存安全,引入生命周期来确保引用的有效性。为了验证返回的引用是否有效,我们需要确定其生命周期。但如何确定它呢?
Rust 可以自动推断参数和返回值的生命周期。然而,这种推断并非普遍适用;Rust 只能在三种特定情况下推断生命周期。上述代码并不属于这中情况之一。在这中情况下,我们必须手动标注生命周期。如果没有显式标注,Rust 的借用检查器将无法确定返回值的生命周期。
返回值来自参数。确保返回值与参数具有相同的生命周期是否足够?至少在函数调用的范围内,这将确保引用有效。但是,由于有两个参数,它们的生命周期可能不同。
返回值应该与哪个参数关联?解决方法很简单:返回值应与生命周期最短的参数具有相同的生命周期。这样,返回值在两个参数保持有效。因此,上面代码中的标注 'a 表示返回值的生命周期是两个 'a 参数生命周期的交集。
生命周期与内存管理
Rust 使用生命周期来管理内存。当变量超出作用域时,它所占用的内存将被释放。如果引用指向的内存已经被释放,它就会变成悬空引用,尝试使用它将导致编译错误。
fn main() {
let r;
{
let x = 5;
r = &x;
}
println!("r: {}", r);
}
在上面的代码中,变量 x 在超出作用域时被释放,但变量 r 仍然持有对它的引用。这创建了一个悬空引用。
为什么需要生命周期?
防止悬空引用和确内存安全
如前所述,Rust 使用生命周期来防止悬空引用。
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {}", result);
}
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
在上面的代码中,函数 longest 返回一个字符串切片的引用。编译器检查返回值的生命周期是否有效。
这里还有一个例子,展示了 Rust 如何通过生命周期确保内存安全:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
let string1 = String::from("long string is long");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
}
println!("The longest string is {}", result);
}
在这个代码中,我们定义了一个名为 longest 的函数,它接受两个字符串切片作为参数并返回一个字符串切片。该函数使用生命周期参数 'a 来指定输入参数和返回值之间的生命周期关系。
在 main 函数中,我们创建了两个字符串变量 string1 和 string2,并将它们的切片传递给 longest。由于 longest 要求输入参数和返回值具有相同的生命周期,编译器将检查切片是否满足此要求。这里,string2 的生命周期比 string1 短,因此编译器报告错误,警告返回值可能包含悬空引用。这种机制确保了内存安全。
生命周期语法
生命周期标注
在函数定义中,可以使用尖括号标注生命周期参数。生命周期参数名称必须以撇号开头,例如 'a。
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
在上面的代码中,函数 longest 有两个输入参数,它们都是字符串切片的引用。返回值也具有生命周期参数 'a,表示其生命周期与输入参数相同。
生命周期省略规则
在许多情况下,Rust 编译器可以自动推断引用的生命周期,允许省略生命周期标注。
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
在这种情况下,编译器无法确定参数和返回值的生命周期。
当编译器无法确定函数返回值的生命周期时,它会报错,要求开发者显式指定生命周期参数。例如,我们可以将 longest 函数修改如下:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
在这里,生命周期参数 'a 指定参数和返回值具有相同的生命周期。
然而,在许多情况下,Rust 编译器可以自动推断生命周期。Rust 应用了一组 生命周期省略规则 来推断正确的生命周期。这些规则如下:
- 每个引用参数都有自己的生命周期参数。例如,
fn foo(x: &i32)转换为fn foo<'a>(x: &'a i32)。 - 如果函数有单一的参数,该生命周期将分配给所有输出参数。例如,
fn foo<'a>(x: &'a i32) -> &i32转换为fn foo<'a>(x: &'a i32) -> &'a i32。 - 如果函数有多个输入生命周期参数,但其中一个参数是
&self或&mut self,则返回值将获得self的生命周期。例如,fn foo(&self, x: &i32) -> &i32转换为fn foo<'a, 'b>(&'a self, x: &'b i32) -> &'a i32。
这些规则使 Rust 编译器在许多情况下能够自动推断生命周期。然而,在复杂场景中,编译器可能仍需要显式生命周期标注。
生命周期的使用场景
参数和返回值
当函数的输入参数或返回值包含引用时,必须使用生命周期来确保这些引用的有效性。
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
在上面的代码中,函数 longest 有两个输入参数,它们都是字符串切片的引用。
结构体定义
当结构体包含引用时,必须使用生命周期来确保引用的有效性。
struct ImportantExcerpt<'a> {
part: &'a str,
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().expect("Could not find a '.'");
let i = ImportantExcerpt { part: first_sentence };
}
在上面的代码中,结构体 ImportantExcerpt 包含一个对字符串切片的引用。这个引用具有生命周期参数 'a,表示它必须具有明确定义的生命周期。为了防止悬空引用,字符串切片必须与结构体具有相同的生命周期,确保只要结构体有效,字符串切片也有效。
生命周期的高级用法
生命周期子类型和多态
Rust 支持 生命周期子类型 和 多态。生命周期子类型意味着一个生命周期可以是另一个生命周期的子集。
fn longest<'a>(x: &'a str, y: &str) -> &'a str {
x
}
在这个示例中,第一个输入参数具有生命周期 'a,而第二个输入参数没有显式生命周期标注。这意味着第二个输入参数可以具有任何生命周期,并且它不会影响返回值。
静态生命周期
Rust 有一个特殊的生命周期,称为 'static,表示引用在整个程序运行期间有效。
let s: &'static str = "I have a static lifetime.";
在这个示例中,变量 s 是对具有 静态生命周期 的字符串切片的引用,表示它在整个程序执行期间保持有效。
生命周期与借用检查器
借用检查器的作用
Rust 编译器包含一个 借用检查器,确保所有引用都遵循借用规则。如果违反了规则,编译器将生成错误。
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
let r3 = &mut s;
println!("{}, {}, and {}", r1, r2, r3);
}
在这个代码中,变量 s 同时具有不可变引用(r1 和 r2)和可变引用(r3)在同一个作用域中。这违反了 Rust 的借用规则。编译器检测到此问题并生成错误。
生命周期检查确保引用在其整个存在期间保持有效。然而,具有相同的生命周期并不一定意味着可以借用。Rust 的借用规则同时考虑 生命周期有效性 和 可变性约束。
在上面的代码中,尽管 r1、r2 和 r3 具有相同的生命周期,但它们违反了 Rust 的借用规则,因为它们试图在同一个作用域内对同一个变量创建不可变和可变引用。根据 Rust 的借用规则:
- 你可以同时拥有 多个不可变引用 到一个变量。
- 你可以拥有 一个可变引用,但不能同时拥有其他引用(可变或不可变)。
这确保了内存安全并防止数据竞争。
生命周期的局限性
尽管 Rust 使用生命周期来管理内存并确保安全,但生命周期也有一些局限性。例如,在某些情况下,编译器 无法自动推断 正确的生命周期,需要开发者显式标注。这可能会增加开发者的负担并降低代码的可读性。