生命周期

  • 分配在堆和栈上的内存有其各自的作用域,它们的生命周期是动态的。
  • 全局变量、静态变量、字符串字面量、代码等内容,在编译时,会被编译到可执行文件中的 BSS/Data/RoData/Text 段,然后在加载时,装入内存。因而,它们的生命周期和进程的生命周期一致,所以是静态的。
  • 所以,函数指针的生命周期也是静态的,因为函数在 Text 段中,只要进程活着,其内存一直存在。

生命周期标注的目的是,在参数和返回值之间建立联系或者约束。调用函数时,传入的参数的生命周期需要大于等于(outlive)标注的生命周期。

在 Rust 中,除非显式地做 Box::leak() / Box::into_raw() / ManualDrop 等动作,一般来说,堆内存的生命周期,会默认和其栈内存的生命周期绑定在一起。

在这种默认情况下,在每个函数的作用域中,编译器就可以对比值和其引用的生命周期,来确保“引用的生命周期不超出值的生命周期”。

根据所有权规则,值的生命周期可以确认,它可以一直存活到所有者离开作用域;而引用的生命周期不能超过值的生命周期。在同一个作用域下,这是显而易见的。然而,当发生函数调用时,编译器需要通过函数的签名来确定,参数和返回值之间生命周期的约束。

值的生命周期

如果一个值的生命周期贯穿整个进程的生命周期,那么我们就称这种生命周期为静态生命周期。

当值拥有静态生命周期,其引用也具有静态生命周期。我们在表述这种引用的时候,可以用 'static 来表示。比如: &'static str 代表这是一个具有静态生命周期的字符串引用。

一般来说,全局变量、静态变量、字符串字面量(string literal)等,都拥有静态生命周期。我们上文中提到的堆内存,如果使用了 Box::leak 后,也具有静态生命周期。

如果一个值是在某个作用域中定义的,也就是说它被创建在栈上或者堆上,那么其生命周期是动态的。

当这个值的作用域结束时,值的生命周期也随之结束。对于动态生命周期,我们约定用 'a 、'b 或者 'hello 这样的小写字符或者字符串来表述。

译器如何识别生命周期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn main() {
let s1 = String::from("Lindsey");
let s2 = String::from("Rosie");

let result = max(&s1, &s2);

println!("bigger one: {}", result);
}

fn max(s1: &str, s2: &str) -> &str {
if s1 > s2 {
s1
} else {
s2
}
}

编译器在编译 max() 函数时,无法判断 s1、s2 和返回值的生命周期。

当出现了多个参数,它们的生命周期可能不一致时,返回值的生命周期就不好确定了。编译器在编译某个函数时,并不知道这个函数将来有谁调用、怎么调用,所以,函数本身携带的信息,就是编译器在编译时使用的全部信息。

根据这一点,我们再看示例代码,在编译 max() 函数时,参数 s1 和 s2 的生命周期是什么关系、返回值和参数的生命周期又有什么关系,编译器是无法确定的。

此时,就需要我们在函数签名中提供生命周期的信息,也就是生命周期标注(lifetime specifier)。在生命周期标注时,使用的参数叫生命周期参数(lifetime parameter)。通过生命周期标注,我们告诉编译器这些引用间生命周期的约束。

生命周期参数的描述方式和泛型参数一致,不过只使用小写字母。这里,两个入参 s1、 s2,以及返回值都用 ‘a 来约束。生命周期参数,描述的是参数和参数之间、参数和返回值之间的关系,并不改变原有的生命周期。

在我们添加了生命周期参数后,s1 和 s2 的生命周期只要大于等于(outlive) ‘a,就符合参数的约束,而返回值的生命周期同理,也需要大于等于 ‘a 。

引用需要额外标注吗

1
2
3
4
5
6
7
8
9
10
11
12
13
fn main() {
let s1 = "Hello world";

println!("first word of s1: {}", first(&s1));
}

fn first(s: &str) -> &str {
let trimmed = s.trim();
match trimmed.find(' ') {
None => "",
Some(pos) => &trimmed[..pos],
}
}

虽然我们没有做任何生命周期的标注,但编译器会通过一些简单的规则为函数自动添加标注:

  • 所有引用类型的参数都有独立的生命周期 ‘a 、’b 等。
  • 如果只有一个引用型输入,它的生命周期会赋给所有输出。
  • 如果有多个引用类型的参数,其中一个是 self,那么它的生命周期会赋给所有输出。

例子中的 first() 函数通过规则 1 和 2,可以得到一个带生命周期的版本:

1
2
3
4
5
6
7
fn first<'a>(s: &'a str) -> &'a str {
let trimmed = s.trim();
match trimmed.find(' ') {
None => "",
Some(pos) => &trimmed[..pos],
}
}

你可以看到,所有引用都能正常标注,没有冲突。那么对比之前返回较大字符串的示例代码(示例代码), max() 函数为什么编译器无法处理呢?

按照规则 1, 我们可以对 max() 函数的参数 s1 和 s2 分别标注’a 和’b ,但是返回值如何标注?是 ‘a 还是’b 呢?这里的冲突,编译器无能为力。

1
fn max<'a, 'b>(s1: &'a str, s2: &'b str) -> &'??? str

明白了代码逻辑,才能正确标注参数和返回值的约束关系,顺利编译通过。