11 KiB
使用生命周期验证引用
生命周期是我们已经使用过的另一种通用类型。生命周期不是确保一个类型具有我们想要的行为,而是确保引用在我们需要时有效。每个引用都有生命周期,这是该引用的有效范围。
大多数时候,生命周期是隐式的和推断的,就像大多数时候,类型是推断的一样。只有在可能存在多种类型时,我们才必须注释类型。以类似的方式,当引用的生命周期可以通过几种不同的方式相关时,我们必须注释生命周期。Rust 要求我们使用通用生命周期参数来注释关系,以确保在运行时使用的实际引用绝对有效。
使用生命周期防止悬挂引用
fn main() {
let r;
{
let x = 5;
r = &x;
}
println!("r: {}", r);
}
x在内部范围外失效,在尝试使用被引用的x时,已超出了范围。故而无法编译。
借用检查器
Rust 编译器有一个借用检查器,它比较范围以确定所有借用是否有效。
fn main() {
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {}", r); // |
} // ---------+
这里,我们用'a注释了r的生命周期,用'b注释了x的生命周期。在编译时,Rust比较两个生存期的大小,并看到r的生存期为'a,但它引用的内存的生存期为'b。程序被拒绝,因为'b比'a短:引用对象的生存期没有引用的生存期长。
fn main() {
let x = 5; // ----------+-- 'b
// |
let r = &x; // --+-- 'a |
// | |
println!("r: {}", r); // | |
// --+ |
} // ----------+
这里,x的生命周期为'b,在这种情况下大于'a。这意味着r可以引用x,因为Rust知道r中的引用总是有效的,而x是有效的。
函数中的通用生命周期
只有当一个函数的参数同时满足以下两个条件时,才需要使用生命周期注解:
- 参数是一个引用类型,即 &T 或 &mut T。
- 参数在函数体内被使用,且函数返回一个引用类型,即 &T 或 &mut T。
在这种情况下,编译器需要知道引用的生命周期,以便确保函数返回的引用不会超出参数引用的生命周期。如果参数是在堆上分配的,而不是引用类型,则无需使用生命周期注解。例如,Box 类型的参数就不需要使用生命周期注解。
函数将接受两个字符串切片,并返回一个(较长)字符串切片。
错误原因:因为Rust无法判断所返回的引用是指向x还是y。且当参数传递值有误时,if-else不会执行,故而不能确定返回的引用是否总有效。
生命周期注释语法
生命周期不会改变任何引用的生命期。它们描述了多个引用彼此之前生存期的关系。函数可以接受任何生命期的引用,通过指定泛型生命期参数。
生命周期注释的语法有点不同寻常:生命周期参数的名称必须以撇号(')开头,通常都是小写的,而且非常短,就像泛型类型一样。大多数人使用名称'a '作为第一个生命周期注释。我们将生命周期参数注释放在引用的&之后,使用一个空格将注释与引用的类型分开。
&i32 // a reference
&'a i32 // a reference with an explicit lifetime
&'a mut i32 // a mutable reference with an explicit lifetime
一个生命周期注释本身没有什么意义,这些注释是为了告诉Rust多个引用的通用生命周期参数如何相互关联。
函数签名中的生命周期注释
要在函数签名中使用生命期注释,需要在函数名和参数列表之间的尖括号内声明泛型生命期参数。
该签名表达以下约束:只要两个参数都有效,返回的引用就有效。这是参数的生存期和返回值之间的关系。我们将生命周期命名为'a,然后将它添加到每个引用中,
函数签名现在告诉Rust,对于某个生命期'a,函数接受两个形参,这两个形参都是至少与生命期'a一样长的字符串切片。函数签名还告诉Rust,从函数返回的字符串切片将至少与生命期'a一样长。在实践中,这意味着最长函数返回的引用的生命期与函数参数引用的值的生命期中较小的那个相同。这些关系就是我们在分析代码时希望Rust使用的关系。
在函数中注释生命周期时,注释放在函数签名中,而不是函数体中。生命周期注释成为函数契约的一部分,就像签名中的类型一样。让函数签名包含生命周期契约意味着Rust编译器所做的分析可以更简单。
当我们将具体引用传递给longest时,被替换为'a的具体生命期是x的作用域中与y的作用域重叠的部分。换句话说,泛型生命期'a将获得等于x和y的生命期中较小的那部分的具体生命期。
一种情况:
在本例中,string1在外部作用域结束之前都有效,string2在内部作用域结束之前都有效,result引用的值在内部作用域结束之前都有效。故可以运行。
另一种情况:此代码会返回string1的引用,同时string1还未超出作用域,按理来说可以打印。但是rust只认生存期中较小的。故而报错。
Thinking in Terms of Lifetimes
指定生命周期参数的方式取决于函数正在执行的操作。eg:若只返回第一个形参,而不是最长的,就不需要在y上指定生存期。
当指定返回类型生命周期,但参数与该生命周期无关时:
修改方法:
Lifetime Annotations in Struct Definitions
以下情况需要在结构定义中的每个引用上添加一个生命周期注释。
生命周期注解意味着 ImportantExcerpt 的实例不能超出它在 part和part2 字段中所持有的引用的生命周期。
使用String时:
Lifetime Elision
每个引用都有生命周期。需为引用的函数或结构指定生命周期参数。但是也有一些函数会有特殊情况。下面函数编译时没有生命周期。(参数和返回类型是引用)
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
在Rust早期(pre-1.0)此代码不会被编译,因为每个引用都需要一个明确的生命周期。故而签名应写成这样:
fn first_word<'a>(s: &'a str) -> &'a str {
但是随着发展,rust引用分析中添加了 生命周期省略规则。 这是编译器会考虑的一组特定情况,如果代码符合此情况,则无需显式编写生命周期。
函数或方法参数的生命周期称为输入生命周期,返回值的生命周期称为输出生命周期。
当没有显式注释时,编译器使用三个规则来确定引用的生命周期。第一个规则适用于输入生命周期,第二条和第三条规则适用于输出生命周期。如果编译器到达三个规则的末尾仍然有它无法确定生命周期的引用,编译器将停止并报错。规则适用于fn定义和impl块。
规则1:编译器为每个引用参数分配一个生命周期参数。具体来讲,具有一个参数的函数获得一个生命周期参数: fn foo<'a>(x: &'a i32); 有两个参数的函数有两个独立的生命周期参数:fn foo<'a, 'b>(x: &'a i32, y: &'b i32)。以此类推。
规则2:如果只有一个输入生命周期参数,则该生命周期将分配给所有输出生命周期参数:fn foo<'a>(x: &'a i32) -> &'a i32。
规则3(仅使用方法签名):如果有多个输入生命周期参数,但其中一个是 &self 或 &mut self,因为这是一个方法,self 的生命周期将分配给所有输出生命周期参数。这第三条规则使得方法更易于阅读和编写,因为需要的符号更少。
举个例子:
// 我们定义的
fn first_word(s: &str) -> &str {
// 第一条规则适用:经过第一条规则后
fn first_word<'a>(s: &'a str) -> &str {
// 第二条规则适用:经过第二条规则后
fn first_word<'a>(s: &'a str) -> &'a str {
// 满足编译器要求,每个引用都有生命周期
// 另外定义
fn longest(x: &str, y: &str) -> &str {
// 第一条规则适用:经过第一条规则后
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {
// 第二条规则不适应,第三条也不适用
// 返回值无生命周期,无法编译。
Lifetime Annotations in Method Definitions
根据生命周期省略规则的第一条规则,Rust 会为 &self
和 announcement
分别分配各自的生命周期。然后,由于其中一个参数是 &self
,所以返回类型的生命周期就等同于 &self
的生命周期。
impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {}", announcement);
self.part
}
}
The Static Lifetime
特殊的生命周期static
,表示受影响的引用可以在程序的整个生命周期内存在。所有的字符串字面值都有 static
生命周期。因此它始终可用。因此,所有字符串字面值的生命周期都是 static
。
不要使用static来解决悬挂引用或生命周期不匹配问题。应修复问题,而不是直接把生命周期设为整个程序/
Generic Type Parameters, Trait Bounds, and Lifetimes Together
示例代码: