Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

10.7 生命周期 Pt.3:输入输出生命周期与3规则

10.7.1. 深入理解生命周期

1. 指定生命周期参数的方式取决于函数所做的事情

10.6 生命周期 Pt.2:生命周期的语法与例子 的代码为例:

#![allow(unused)]
fn main() {
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {  
    if x.len() > y.len() {  
        x  
    } else {  
        y  
    }  
}
}

这个函数签名之所以这样写,是因为不知道返回值到底是 x 还是 y。如果我修改代码,把返回值固定为 x,那就不必再给 y 显式生命周期了:

#![allow(unused)]
fn main() {
fn longest<'a>(x: &'a str, y: &str) -> &'a str {  
    x
}
}

因此,这个函数签名并没有约束 y 的生命周期。

2. 当函数返回引用时,返回类型的生命周期参数必须与其中一个输入生命周期匹配

如果返回的引用没有指向任何参数,返回的内容就会变成悬空引用,因为在函数内创建的值会在函数结束时离开作用域,而返回的引用指向的是已经被释放的内存。

看这个例子:

#![allow(unused)]
fn main() {
fn longest<'a>(x: &'a str, y: &str) -> &'a str {  
    let result = String::from("Something");
    result.as_str()
}
}

在这个函数中,创建了一个名为 resultString 值,然后对 result 调用 as_str 方法返回一个字符串切片(&str),其实就是一个引用。这样就会报错:

error[E0515]: cannot return value referencing local variable `result`
 --> src/main.rs:3:5
  |
3 |     result.as_str()
  |     ------^^^^^^^^^
  |     |
  |     returns a value referencing data owned by the current function
  |     `result` is borrowed here

报错信息说无法返回引用本地变量 result 的值,因为返回的值是函数自身拥有的数据。这与刚才提到的原因相同:一旦内部数据离开作用域,就会被清理。

如果我想返回在函数内部创建的值呢?那就不要返回引用,直接返回这个值:

#![allow(unused)]
fn main() {
fn longest(x: &str, y: &str) -> String {  
    let result = String::from("Something");
    result
}
}

这相当于把函数中该值的所有权转移给调用者,并由调用者负责清理这块内存。这种写法也不需要显式生命周期,因为返回值与参数无关,而且只有引用才会有生命周期问题。

从这个例子可以看出,生命周期语法从根本上就是用来关联函数中不同参数和返回值的生命周期的。 一旦建立起这些关系,Rust 就有足够的信息来支持保证内存安全的操作,并拒绝可能导致悬空指针或其他破坏内存安全的操作。

10.7.2. 结构体中的生命周期标注

在前面的文章中,我们在结构体中只定义过自持有类型,例如 i32String。实际上,结构体字段也可以是引用类型;如果是引用,就需要给每个引用添加生命周期标注。

看这个例子:

struct ImportantExcerpt<'a> {
    part: &'a str,
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().unwrap();
    let i = ImportantExcerpt {
        part: first_sentence,
    };
}

ImportantExcerpt 只有一个字段 part,其类型是字符串切片,也就是引用类型。因为它是引用类型,所以需要生命周期标注。

标注生命周期的方式与泛型相同:在结构体名称后面加上 <>,并在里面写入生命周期泛型参数。这里是 'apart 这个引用必须比结构体实例本身活得更久。只要实例存在,part 引用也必须存在;如果 part 先消失,实例肯定会无效。

main:它先创建了一个名为 novelString,然后使用 splitnext 从字符串中提取第一句(unwrap 用于解包 Option 类型,这在 9.2. Result枚举与可恢复的错误 Pt.1 中介绍过)。这个句子的类型是 &str,也就是一个引用。然后创建了 ImportantExcerpt 的实例 i,并把这个引用作为 part 字段的值。

这样写是有效的,因为 first_sentence 的作用域是从第 7 行到第 11 行,而 i 的作用域是从第 8 行到第 11 行。因此 part 字段活得比实例更久,并且完全覆盖了 i 的生命周期。

10.7.3. 生命周期省略

每个引用都有生命周期,使用生命周期的函数或结构体需要生命周期参数。

那么,为什么这段来自 4.5. 切片(Slice) 的代码,在没有任何生命周期标注的情况下也能通过编译呢?

fn main() {
	let s = String::from("Hello world");
	let word = first_word(&s);
	println!("{}", word);
}
fn first_word(s:&str) -> &str {
	let bytes = s.as_bytes();
	for (i, &item) in bytes.iter().enumerate() {
		if item == b' ' {
			return &s[..i];
		} 
	}
	&s[..]
}

这个函数在没有生命周期标注的情况下也能编译,有其历史原因:在早期版本的 Rust(1.0 之前),这段代码无法通过编译,因为当时要求每个引用都必须有显式生命周期。函数签名必须写成这样:

#![allow(unused)]
fn main() {
fn first_word<'a>(s: &'a str) -> &'a str {
}

后来,Rust 团队发现在某些情况下,Rust 程序员会一遍又一遍地写相同的生命周期标注,而这些情况是可预测的。它们有清晰的模式,于是 Rust 团队把这些模式直接编入编译器,让借用检查器在这些情况下可以自动推断生命周期,而无需程序员显式标注。

了解这段历史的意义在于:未来可能会发现更多确定性的模式,并把它们加入编译器。将来可能需要写的生命周期标注会更少。谢天谢地。

Rust 引用分析中内置的这些模式叫做生命周期省略规则。程序员不需要手动遵循它们;它们是由编译器处理的特殊情况。如果你的代码符合这些情况,就不需要显式生命周期标注。

不过,生命周期省略并不提供完整推断。如果应用规则之后引用仍然不明确,仍然会发生编译错误。解决办法是手动添加生命周期,以表明引用之间的关系。

10.7.4. 输入、输出生命周期

如果生命周期出现在函数或方法的参数中,就叫做输入生命周期

如果它出现在函数或方法的返回值中,就叫做输出生命周期

10.7.5. 生命周期省略的三个规则

编译器使用三条规则,在没有显式标注生命周期时确定生命周期:

  • 规则 1 用于输入生命周期
  • 规则 2 和 3 用于输出生命周期
  • 如果编译器在应用完三条规则后仍然无法确定生命周期,就会报错
  • 这三条规则不仅适用于函数或方法定义,也适用于 impl

规则 1: 每个引用参数都会获得自己的生命周期。 单参数函数有一个生命周期,双参数函数有两个生命周期,以此类推。

规则 2: 如果恰好只有一个输入生命周期参数,那么该生命周期会被赋给所有输出生命周期参数。 换句话说,如果只有一个输入生命周期,那么这个生命周期就是该函数所有可能返回值的生命周期。

规则 3: 如果有多个输入生命周期参数,但其中一个是 &self&mut self(也就是说,这个函数是方法),那么 self 的生命周期会被赋给所有输出生命周期参数。

1. 成功例

规则已经清楚了,现在看一个例子:

#![allow(unused)]
fn main() {
fn first_word(s:&str) -> &str {
	//...
}
}

把自己代入编译器,想想如何用这三条规则找出这个函数签名中省略的生命周期。

首先应用规则 1——每个引用参数都会获得自己的生命周期。这里只有一个参数,所以只有一个生命周期。此时编译器推断出:

#![allow(unused)]
fn main() {
fn first_word<'a>(s:&'a str) -> &str {
	//...
}
}

因为只有一个输入生命周期,规则 2 在这里也适用——如果恰好只有一个输入生命周期参数,那么该生命周期会被赋给所有输出生命周期参数。因此输入生命周期被赋给了输出生命周期。此时编译器推断出:

#![allow(unused)]
fn main() {
fn first_word<'a>(s:&'a str) -> &'a str {
	//...
}
}

因为只有一个输入生命周期,且这个函数不是方法,所以规则 3 不适用。

现在函数中的每个引用都有了生命周期,因此编译器可以继续分析代码,而无需程序员手动标注函数签名中的生命周期。

2. 失败例

看第二个例子:

#![allow(unused)]
fn main() {
fn longest(x:&str, y:&str) -> &str {
	//...
}
}

这个函数签名有两个引用输入,返回类型也是引用。尝试应用这三条规则:

首先应用规则 1——每个引用参数都会获得自己的生命周期。这里有两个参数,所以有两个生命周期:

#![allow(unused)]
fn main() {
fn longest<'a, 'b>(x:&'a str, y:&'b str) -> &str {
	//...
}
}

因为有两个引用参数,规则 2 不适用。

因为这个函数不是方法,规则 3 不适用。

应用完三条规则后,返回值的生命周期仍然无法确定,所以编译器会报错。也就是说,你必须显式声明生命周期。