Richard's Blog

Rust中的变量生存期——lifetime

字数统计: 2k阅读时长: 7 min
2019/08/20 Share

在之前的文章中,关于引用与借取的内容有一点没有详细说明,就是在Rust中的所有引用都有生存期(lifetime),它是用于表达引用的作用范围。

大多数情况下,生存期都是隐式推断的,但是如果有多种可能导致无法推断,就必须使用泛型生存期参数进行声明。

生存期这个概念大概是Rust语言中最具特色的特点,这篇文章只简要解释常见的生存期语法。

使用生存期来预防悬空引用

生存期的主要目的是确保在程序中引用到预期的数据,防止悬空引用。

1
2
3
4
5
6
7
8
9
10
{
let r;

{
let x = 5;
r = &x;
}

println!("r: {}", r);
}

上述代码会产生编译错误,错误原因是r虽然在外部代码块被定义了,然后在内部代码块借取了x的引用,但是x的owne在内部代码结束时被drop了,当执行到打印的语句时,r已经是悬空引用了,所以会在编译时出错。

1
2
3
4
5
6
7
8
9
10
error[E0597]: `x` does not live long enough
--> src/main.rs:7:5
|
6 | r = &x;
| - borrow occurs here
7 | }
| ^ `x` dropped here while still borrowed
...
10 | }
| - borrowed value needs to live until here

借取检查器(The Borrow Checker)

Rust编译器有一个借取检查器用于比较作用域,确定所有借取是否都可用。

以下是所有变量的作用域

1
2
3
4
5
6
7
8
9
10
{
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {}", r); // |
} // ---------+

可以看到r的生存期是'a,x的生存期是'b'b生存期的块要明显小于外面'a生存期的块。

当程序编译时,编译器会比较两个生存期,并且发现'a生存期中的变量r引用了'b生存期中的变量x,此时程序会中止,因为'b生存期比'a生存期短:引用主体存活时间与引用不一致。

如果更改成以下的形式就没有问题

1
2
3
4
5
6
7
8
{
let x = 5; // ----------+-- 'b
// |
let r = &x; // --+-- 'a |
// | |
println!("r: {}", r); // | |
// --+ |
} // ----------+

此时'b生存期比'a生存期大,Rust编译器能知道在变量r的使用范围里x的引用都是有效的

方法中的泛型生存期

1
2
3
4
5
6
7
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}

以上方法是用于返回最长的字符串切片。方法参数为字符串切片是因为我们不想传入字符串所有权。

以上方法是无法通过编译的,会得到以下错误提示

1
2
3
4
5
6
7
8
error[E0106]: missing lifetime specifier
--> src/main.rs:1:33
|
1 | fn longest(x: &str, y: &str) -> &str {
| ^ expected lifetime parameter
|
= help: this function's return type contains a borrowed value, but the
signature does not say whether it is borrowed from `x` or `y`

错误提示我们缺少生存期提示符,因为Rust编译器不能判断返回的字符串切片是x的还是y的。事实上,写下这段代码的我们也不知道会返回哪个引用。所有我们无法向上面一样分析xy的生存期范围在哪里,此时我们就需要使用泛型生存期参数来定义引用间的关系,告诉借取检查器如何分析。

生存期声明的语法

声明生存期并不会改变引用的存活时间,正如函数在签名指定泛型类型参数时可以接受任何类型一样,函数也可以通过指定泛型生存期参数来接受任何生存期的引用。

生存期的声明语法在编程语言中不太常见:在泛型生存期参数名前必须有一个单引号',通常生存期名是全小写并且非常短,就像泛型类型一样。比如人们常用'a作为生存期名。生存期参数声明是放在一个引用的&后面,并在引用名间用空格隔开。以下是一些例子。

1
2
3
&i32        // 这是一个引用
&'a i32 // 这是一个带有显示声明生存期的引用
&'a mut i32 // 这是一个带有显示声明生存期的可变引用

单独声明一个生存期没什么意义,声明生存期是为了在多个引用存在的时候告诉Rust这些引用的泛型生存期参数之间的关系。

在函数签名中的生存期声明

我们用上面longest方法作例子,声明一个'a生存期,然后把这个生存期作用到所有引用上

1
2
3
4
5
6
7
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}

以上代码是可以编译成功并且能返回我们想要的结果。

函数签名告诉Rust函数的两个参数有效范围与'a生存期一致,返回值的有效范围也与'a生存期一致。

注意,函数返回值的生存期总是等于传入参数的生存期的最小值。

以下为调用此函数的代码

1
2
3
4
5
6
7
8
9
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);
}

以程序员视角看,这段代码最终会打印出string1的值,但是上面提到,函数的返回值生存期总是等于传入参数中生存期的最小值。所以此处生存期最小的是string2的生存期,所以result的生存期跟string2一致。当代码执行到println!处时,result变量已经变为悬空引用了。

生存期的隐式规则

在Rust的历史版本1.0中,所有的函数都需要显式声明参数与返回值的生存期。后来随着用户编写了越来越多的代码后,发现大多数函数参数与返回值的生存期都是可以被推断的,于是Rust的开发者们让Rust编译器在Rust程序编译时会按照一定的规则自动添加函数的参数与返回值生存期,只有在编译器无法通过规则判断生存期时以错误的方式提示程序员需要显示指定生存期。

我们把在函数参数声明的生存期称为输入生存期,把声明在返回值的生存期称为输出生存期

在当前版本的Rust编译器中对于隐式添加生存期有三个规则

规则一

函数中每个参数都有一个独立的生存期:fn foo<'a, 'b>(x: &'a i32, y: &'b i32)

规则二

如果函数只有一个参数,则此参数的生存期会被分配到所有的输出生存期上:fn foo<'a>(x: &'a i32) -> &'a i32

规则三

如果函数有多个参数,但是有一个参数是&self&mut self,则self的生存期会被分配到所有的输出生存期上。

静态生存期

被静态生存期声明的引用会存活在整个程序执行的过程中。声明静态生存期的标识为'static,所有的字符串字面量都是'static的生存期。

有可能在一些错误提示中会提示你将变量设置为'static,一般是在一些生存期异常或试图创建悬空指针时出现,在创建静态生存期前应该先考虑是否需要变量在整个程序运行期内有效,并先尝试解决问题而不是指定静态生存期。

CATALOG
  1. 1. 使用生存期来预防悬空引用
  2. 2. 借取检查器(The Borrow Checker)
  3. 3. 方法中的泛型生存期
  4. 4. 生存期声明的语法
  5. 5. 在函数签名中的生存期声明
  6. 6. 生存期的隐式规则
    1. 6.1. 规则一
    2. 6.2. 规则二
    3. 6.3. 规则三
  7. 7. 静态生存期