Rust生命周期误解翻译

生命周期误解的意译版 + 个人批注

前言

  • 原文链接
  • 为什么要翻译:强制放慢阅读速度,加深对内容的理解;分享自己的一些理解和总结
  • 如何翻译的:主要是意译,有意将长句子拆开,便于阅读,同时加入了一些承上启下的句子,补充更容易理解的行文逻辑。另外还添加了一些结论性的注释,论据主要来自于Ownership - The Rustonomicon,作为不同视角的参考。
  • 应该如何阅读:时刻参照原文。根据我读译文的经验,不能过分相信翻译者!当读起来不顺畅,有异样感觉时,就看原文吧,可能找到导向更好理解的线索。

引言

本文所列出的对生命周期的误解,我过去都经历过,而如今,我发现很多初学者依然身陷其中。

可能我所用的术语并不标准,因此列出下表,阐明我将使用的词语,以及对应的含义

短语 含义
T 1. 一个集合,包含所有可能的类型
2. 在该集合中的某个类型
所有权类型 非引用的类型,比如i32,String,Vec
1. 借用类型 或者
2. 引用类型
无关可变性的引用类型,比如&i32, &mut i32
1. 可变引用(借用) 或者
2. 排它引用(借用)
具有排它性、唯一性的可变引用,比如&mut T
1. 不可变引用 或者
2. 共享引用
共享的不可变引用,比如&T

误解

概括一下,一个变量的生命周期是指,变量所指向的内存数据,在其当前内存地址上可以保持多长的有效性,该有效性由编译器静态验证。

Rutonomicon定义:Lifetimes are named regions of code that a reference must be valid for. 有名称的代码作用域,在作用域中引用必须保证有效

1) T只包含所有权类型

相比生命周期,泛型可能是更适合这个误解的主题。但在Rust世界中,泛型和生命周期关联十分紧密,几乎不能单独谈论。

当我开始学习Rust时,我能够理解 i32, &i32, 和&mut i32 属于不同的类型,同时我也理解,泛型T代表着一个类型集合。尽管分开理解没有障碍,但放在一起时就有了问题。在早期,我对Rust泛型的理解是这样的:

Type Variable T &T &mut T
Examples i32Vec<T> &i32&Vec<T> &mut i32, &mut Vec<T>

T是所有权类型集合,&T是不可变借用集合,&mut T则是可变借用集合, T, &T, &mut T 是三个不相交的有限集合。简洁,清晰,容易,符合直觉,但是,完全错了。实际情况是这样的

Type Variable T &T &mut T
Examples i32, &i32, &mut i32, &&i32, &mut &mut i32, … &i32, &&i32, &&mut i32, … &mut i32, &mut &mut i32, &mut &i32,

T, &T, &mut T 都是无限集合,因为你可以对一个类型一直借用,无限套娃。T&T&mut的超集,而&T&mut T确实是不相交的。

下面是几个解释相关概念的样例:

1
2
3
4
trait Trait {}
impl<T> Trait for T {}
impl<T> Trait for &T {} // 编译错误
impl<T> Trait for &mut T {} // 编译错误

编译器并不会允许我们为&T&mut T实现Trait,因为和为T实现的Trait冲突了,正如之前所说,T包含所有的&T&mut T。而如下的程序则可以编译,因为&T&mut T不相交。

1
2
3
trait Trait {}
impl<T> Trait for &T {}
impl<T> Trait for &mut T {}

关键点

  • T&T&mut的超集
  • &T&mut T不相交

2) T: 'static表示T必须对整个程序有效

该误解的推论:

  • T: 'static 读作 “T 拥有 'static 生命周期”
  • &'static TT: 'static 没有区别
  • T: 'static 表示 T 不可修改
  • T: 'static 表示 T 只能在编译期被创建

大部分的Rust初学者,第一次看到'static恐怕是在这样的代码中:

1
2
3
fn main() {
let str_literal: &'static str = "str literal";
}

他们感觉到,"str literal"被硬编码到编译后的二进制文件中,在运行时被载入内存,所以它不可变,并且在整个程序范围内有效,因此被标记为'static。当他们发现还可以使用static关键字声明静态变量时,这种理解被进一步地加强了。

1
2
3
4
5
6
7
8
9
10
11
static BYTES: [u8; 3] = [1, 2, 3];
static mut MUT_BYTES: [u8; 3] = [1, 2, 3];

fn main() {
MUT_BYTES[0] = 99; // 编译错误,修改static变量需要unsafe

unsafe {
MUT_BYTES[0] = 99;
assert_eq!(99, MUT_BYTES[0]);
}
}

静态变量有如下性质:

  • 只在编译期被创建
  • 默认不可变,修改需要unsafe
  • 对整个程序范围有效

所以'static生命周期标记,就是以静态变量默认生命周期命名的,对吧?

所以如果我们断言'static 生命周期也遵守上述静态变量的规则,也理所应当,对吧?

是的,这没错。但是,我们要区分:

  • 一个变量拥有'static生命周期

    具体例子是什么?就是静态变量本身吗?

  • 一个变量被'static生命周期绑定

    这个应该指T: 'static

后者实际上可以在运行时分配内存并创建,也能在safe Rust中自由访问修改,甚至被drop,在任意长度存活。

其实这里我非常很困惑,原文如下:

The 'static lifetime was probably named after the default lifetime of static variables, right? So it makes sense that the 'static lifetime has to follow all the same rules, right?

Well yes, but a type with a 'static lifetime is different from a type bounded by a 'static lifetime. The latter can be dynamically allocated at run-time, can be safely and freely mutated, can be dropped, and can live for arbitrary durations.

这个Well, yes具体肯定的是什么东西?’static lifetime has to follow 一句中'static lifetime是主语,这个主语怎么实施follow这个动作?凡是出现‘static,不都是被绑定的情况吗,with 'static是什么意思体现在哪里呢?

由此我们再区分一个重要的点: &'static TT: 'static

&'static是一个对T的不可变引用,可以在任意长的范围内被持有,甚至到程序终止。这当然要求T本身是不可变的(创建共享引用后,原则上不再被修改),并且在创建引用后不会移动。T不必在编译时创建,因为我们完全可以在运行时动态分配内存,返回一个'static引用,代价是(可能的)内存泄漏,比如:

1
2
3
4
5
6
7
use rand;

// 运行时动态生成随机 'static str 引用
fn rand_str_generator() -> &'static str {
let rand_string = rand::random::<u64>().to_string();
Box::leak(rand_string.into_boxed_str())
}

T: 'static 不仅包含 &'static T,还包含所有权类型,比如StringVec等等,范围更广。所有权类型保证数据的有效性,只要所有权类型被持有,就保证数据在之后任意长的范围内被持有,甚至到程序终止,自然符合'static的意思。T: 'static不应该被读作 “T 拥有 'static 生命周期”,应该是“T'static生命周期绑定”。

所有权类型本身就是随时创建,允许被持有到程序结束,也可以随时drop。它可以被'static绑定,就很好地反驳了 T: 'static 表示T必须在整个程序范围内有效这一误解

下面这个程序帮我们阐释了这些概念。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
use rand;

fn drop_static<T: 'static>(t: T) {
std::mem::drop(t);
}

fn main() {
let mut strings: Vec<String> = Vec::new();
for _ in 0..10 {
if rand::random() {
// 所有的字符串都是动态随机生成的a
let string = rand::random::<u64>().to_string();
strings.push(string);
}
}

// String属于所有权类型,所以自然被 'static 绑定
for mut string in strings {
// 所有字符串可变,并不需要unsafe code
string.push_str("a mutation");
// 因为满足 'static 绑定,可以传入`drop_static`销毁
drop_static(string); // compiles
}

// 在程序结尾,所有字符串已经被销毁了
println!("i am the end of the program");
}

关键点

  • T: 'static应该被读作“T'static生命周期绑定”。

  • T: 'static 表示T是一个拥有 'static 生命周期的借用 或者 是一个所有权类型

    原文这里用的就是with 'static,人晕了,不应该是绑定吗?

  • 由于T: 'static中的T包含所有权类型,也就意味着T

    • 可以在运行时被动态创建
    • 不必对整个程序有效
    • 可以在safe Rust中自由地修改
    • 可以在运行时被释放
    • 可以有自由的生命周期

3) &'aT:'a含义相同

这个误解是2)的泛化版本

&'a T 的出现,实际上也暗示着 T: 'a。因为如果T本身不能在范围 'a 中有效,那么对 T的引用自然不会在范围 'a 中有效. 比如,Rust 编译期不会允许创建类型 &'static Ref<'a, T> ,因为 Ref 类型本身只在 'a 范围内有效,我们不可能制作一个 'static 引用指向它。

另外一点已经提过,T:'a 包含 &'a T,反之不成立

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 只接受被'a 绑定的引用类型
fn t_ref<'a, T: 'a>(t: &'a T) {}

// 只接受被'a 绑定的任意类型
fn t_bound<'a, T: 'a>(t: T) {}

// 所有权类型,包含一个引用,只对 'a 范围有效
struct Ref<'a, T: 'a>(&'a T);

fn main() {
let string = String::from("string");

t_bound(&string); // compiles
t_bound(Ref(&string)); // compiles
t_bound(&Ref(&string)); // compiles

t_ref(&string); // compiles
t_ref(Ref(&string)); // 编译失败,期望引用类型,
t_ref(&Ref(&string)); // compiles

// string 被 'static 绑定,自然可以由范围更小的'a处理
t_bound(string); // compiles
}

关键点:

  • T:'a&'a T 更广泛,更灵活
  • 如果 T: 'static 那么 T: 'a 也成立,因为对任意 'a都有 'static >= 'a ('static 是所有生命周期类型的子类型)

4) 我的代码没有泛型,而且不需要生命周期

该误解的推论:

  • 有可能完全避免使用泛型和生命周期

这个误解的出现,可能有Rust的生命周期省略规则(lifetime elision rules)的责任。这些规则允许你在函数签名中省略生命周期标注,因为编译器会按照如下的规则进行推断:

  • 每个输入引用都有不同的生命周期
  • 如果只有一个输入引用,则所有输出引用都应用该生命周期
  • 如果&self&mut self出现在输入引用中,那么所有输出引用都应用该生命周期
  • 其他情况下,生命周期参数都必须显示指定

一次性也消化不了这么多规则,我们还是看例子吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 省略
fn print(s: &str);

// 展开
fn print<'a>(s: &'a str);

// 省略
fn trim(s: &str) -> &str;

// 展开
fn trim<'a>(s: &'a str) -> &'a str;

// 非法,没有输入引用,不能推断输出生命周期
fn get_str() -> &str;

// 显式地修复包括:
fn get_str<'a>() -> &'a str; // 泛型
fn get_str() -> &'static str; // 'static

// 非法,有多个输入引用,不能推断输出生命周期
fn overlap(s: &str, t: &str) -> &str;

// 显式地修复(部分仍然应用了省略规则)包括:
fn overlap<'a>(s: &'a str, t: &str) -> &'a str; // 输出引用的有效范围不能大于 s 的有效范围
fn overlap<'a>(s: &str, t: &'a str) -> &'a str; // 输出引用的有效范围不能大于 t 的有效范围
fn overlap<'a>(s: &'a str, t: &'a str) -> &'a str; // 输出引用的有效范围不能大于 t或s 的有效范围
fn overlap(s: &str, t: &str) -> &'static str; // 输出引用的有效范围 **可以** 大于输入的s或者t
fn overlap<'a>(s: &str, t: &str) -> &'a str; // 输出引用的生命周期和输入引用没有关系

// 展开
fn overlap<'a, 'b>(s: &'a str, t: &'b str) -> &'a str;
fn overlap<'a, 'b>(s: &'a str, t: &'b str) -> &'b str;
fn overlap<'a>(s: &'a str, t: &'a str) -> &'a str;
fn overlap<'a, 'b>(s: &'a str, t: &'b str) -> &'static str;
fn overlap<'a, 'b, 'c>(s: &'a str, t: &'b str) -> &'c str;

// 省略
fn compare(&self, s: &str) -> &str;

// 展开
fn compare<'a, 'b>(&'a self, &'b str) -> &'a str;

当你写出:

  • 为结构体实现的方法
  • 有引用参数的函数
  • 返回值是引用的函数
  • 泛型函数
  • trait object
  • 闭包

实际上都会应用被省略的生命周期标记

关键点:

  • 几乎所有Rust代码都是泛型代码,同时到处都有被省略的生命周期标记。

5) 如果程序可以编译,我的生命周期标注就是正确的

该误解的推论

  • Rust的生命周期省略规则总是正确的
  • Rust的借用检查器总是正确的,无论在技术层面还是语义层面
  • Rust比我更了解我程序的语义

Rust 可能编译出一个语义上错误的代码。比如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct ByteIter<'a> {
remainder: &'a [u8]
}

impl<'a> ByteIter<'a> {
fn next(&mut self) -> Option<&u8> {
if self.remainder.is_empty() {
None
} else {
let byte = &self.remainder[0];
self.remainder = &self.remainder[1..];
Some(byte)
}
}
}

fn main() {
let mut bytes = ByteIter { remainder: b"1" };
assert_eq!(Some(&b'1'), bytes.next());
assert_eq!(None, bytes.next());
}

我们实现了一个bytes切片的迭代器,并且成功调用next,看上去一切正常。但是当我们同时持有多个元素,会发生什么呢?

1
2
3
4
5
6
7
8
fn main() {
let mut bytes = ByteIter { remainder: b"1123" };
let byte_1 = bytes.next();
let byte_2 = bytes.next();
if byte_1 == byte_2 {
// do something
}
}

报错了

1
2
3
4
5
6
7
8
9
error[E0499]: cannot borrow `bytes` as mutable more than once at a time
--> src/main.rs:20:18
|
19 | let byte_1 = bytes.next();
| ----- first mutable borrow occurs here
20 | let byte_2 = bytes.next();
| ^^^^^ second mutable borrow occurs here
21 | if byte_1 == byte_2 {
| ------ first borrow later used here

要修复这个错误,我们猜测可以使用Copy,每次迭代都复制u8。这当然可以,但是如果我们扩展ByteIter,变为一个泛型的迭代器,工作在&[T]序列上,数据T的复制可能是一个复杂甚至不可实现的操作,那怎么办呢?好吧,那好像没有什么可以做的了,毕竟代码已经正确编译了,生命周期也没有改进的余地了,对吧?

并不是,其实当前的生命周期标注正是bug的罪魁祸首。而标记的省略,又让这个bug很难被发现。我们先将被省略的标记补充回来,更清楚地观察这个问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct ByteIter<'a> {
remainder: &'a [u8]
}

impl<'a> ByteIter<'a> {
fn next<'b>(&'b mut self) -> Option<&'b u8> {
if self.remainder.is_empty() {
None
} else {
let byte = &self.remainder[0];
self.remainder = &self.remainder[1..];
Some(byte)
}
}
}

好像没什么帮助,仍然让人困惑。Rust专家们有一个建议:给生命周期参数取描述性的名字。我们试试看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct ByteIter<'remainder> {
remainder: &'remainder [u8]
}

impl<'remainder> ByteIter<'remainder> {
fn next<'mut_self>(&'mut_self mut self) -> Option<&'mut_self u8> {
if self.remainder.is_empty() {
None
} else {
let byte = &self.remainder[0];
self.remainder = &self.remainder[1..];
Some(byte)
}
}
}

这样我们能理解了,每个返回的byte引用都是'mut_self标记,但是显然它应该来自于'remainder!让我们修复这一点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
struct ByteIter<'remainder> {
remainder: &'remainder [u8]
}

impl<'remainder> ByteIter<'remainder> {
fn next(&mut self) -> Option<&'remainder u8> {
if self.remainder.is_empty() {
None
} else {
let byte = &self.remainder[0];
self.remainder = &self.remainder[1..];
Some(byte)
}
}
}

fn main() {
let mut bytes = ByteIter { remainder: b"1123" };
let byte_1 = bytes.next();
let byte_2 = bytes.next();
std::mem::drop(bytes); // 我们甚至可以迭代器释放掉
if byte_1 == byte_2 { // 编译通过,我们可以同时持有多个迭代元素
// do something
}
}

现在让我们回看有错误的版本,为什么编译通过了?答案很简单:它内存安全。

Rust借用检查器对生命周期标记的使用,仅停留在静态验证内存安全性,语义层面不关心,即使有错误,只要内存安全就可以编译。比如上面例子中,语义上迭代器需要支持多个迭代引用同时存活,但是只支持一个存活也是内存安全的,所以编译通过,代价是程序变得过分严格,没必要。

修改后可以满足语义,借助的是编译器可以从结构体的字段中自动split出引用 —— rustonomicon。

这里还有一个反面的例子:生命周期的省略正好在语义上正确,我们显示标注的生命周期反而产生了一个“过分严格”的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#[derive(Debug)]
struct NumRef<'a>(&'a i32);

impl<'a> NumRef<'a> {
// NumRef是建立在'a上的泛型,所以需要把self标记为’a,对吗?
// (答案: 不, 这样不对)
fn some_method(&'a mut self) {}
}

fn main() {
let mut num_ref = NumRef(&5);
num_ref.some_method(); // 可变借用num_ref,直到结构体的生命周期结束
num_ref.some_method(); // 编译错误
println!("{:?}", num_ref); // 编译错误
}

如果我们有一个建立在'a上的泛型结构体,我们几乎不会为&'a mut self这个方法接收者编写方法。这样表示告诉Rust,”这个方法需要可变借用该结构体,并且保持有效,直到结构体销毁“。在上面的实际情况中,Rust的借用检查器只会允许对some_method进行一次调用,之后结构体就被永久可变借用,几乎陷入不可使用的状态。当然这个情况非常罕见,但是迷糊的初学者还是容易写出。修复方法就是不要加额外的标记,让省略规则处理它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#[derive(Debug)]
struct NumRef<'a>(&'a i32);

impl<'a> NumRef<'a> {
// 不再添加'a
// 拓展为 fn some_method_desugared<'b>(&'b mut self){}
fn some_method(&mut self) {}
}

fn main() {
let mut num_ref = NumRef(&5);
num_ref.some_method();
num_ref.some_method(); // compiles
println!("{:?}", num_ref); // compiles
}

因为使用了省略规则,some_method使用全新的生命周期参数,调用前新建一个作用域,泛型’b单态化到这个新建的作用域,调用结束后退出作用域,归还可变借用。

关键点:

  • Rust关于函数的生命周期省略规则并不是对任何情况都适用
  • 在你程序的语义层面,Rust了解得有限,并不及你
  • Rust专家建议:给生命周期标记取描述性的名字
  • 在显式放置生命周期标记时,多思考为什么

6) Box托管的trait object没有生命周期

额外参考

Trait object types - The Rust Reference (rust-lang.org)

Lifetime elision - The Rust Reference (rust-lang.org)

std::raw::TraitObject 本身是两个裸指针,其中有指向数据的引用,该引用的有效范围自然也就确定着 trait object 的生命周期。其他包含引用的结构体都会显式声明生命周期,比如ByteIter<'a>,但是dyn SomeTrait不方便采用类似的声明格式,于是加了另一种语法dyn SomeTrait + 'a来表示 struct TraitObject 所绑定的生命周期。而大部分情况下,这种语法的生命周期都是自动推断的,这也是本节讨论的内容。

早前我们讨论了对函数的生命周期省略规则。Rust 对 trait object 也有相应的生命周期省略规则:

  • 如果 trait object 被用做一个外围类型的泛型参数,首先考虑基于该外围类型进行生命周期推断
    • 如果外围类型有唯一的生命周期绑定,沿用它 (对应&'a dyn TraitRef<'a, dyn Trait>)
    • 如果外围类型有多个生命周期绑定,则需要显式指明 trait object 的生命周期 (对应 TwoBounds<'a, 'b, dyn Foo>)
  • 如果上述规则不适用,考虑下列规则:
    • 如果 trait 本身定义包含单个生命周期绑定,默认使用 (对应 dyn GenericTrait<'a>)
    • 如果可以全部 'static , 就用'static . (对应 Box<dyn Trait> ?)
    • 如果不存在生命周期绑定, 则利用表达式推断,非表达式就使用'static (对应 impl dyn Trait)

这些规则听起来超级复杂,但是可以简单地总结为:一个 trait object 的生命周期绑定可由从上下文推断。通过一些例子,我们可以看到这种推断非常符合直觉,所以不必记忆上述规则。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
use std::cell::Ref;

trait Trait {}

// 省略
type T1 = Box<dyn Trait>;
// 展开, Box<T> 中的 T 没有任何生命周期绑定,所以推断为 'static
type T2 = Box<dyn Trait + 'static>;

// 省略
impl dyn Trait {}
// 展开
impl dyn Trait + 'static {}

// 省略
type T3<'a> = &'a dyn Trait;
// 展开, &'a T 要求 T: 'a, 所以推断为 'a
type T4<'a> = &'a (dyn Trait + 'a);

// 省略
type T5<'a> = Ref<'a, dyn Trait>;
// 展开, Ref<'a, T> 要求 T: 'a (Ref内部字段为&'a dyn Trait), 所以推断为 'a
type T6<'a> = Ref<'a, dyn Trait + 'a>;

// 补充来自官方文档的例子
struct TwoBounds<'a, 'b, T: ?Sized + 'a + 'b> {
f1: &'a i32,
f2: &'b i32,
f3: T,
}
type T7<'a, 'b> = TwoBounds<'a, 'b, dyn Foo>;
// ^^^^^^^
// 错误: 这个trait object的生命周期绑定无法从上下文推断

trait GenericTrait<'a>: 'a {}

// 省略
type T8<'a> = Box<dyn GenericTrait<'a>>;
// 展开
type <'a> = Box<dyn GenericTrait<'a> + 'a>;

// 省略
impl<'a> dyn GenericTrait<'a> {}
// 展开
impl<'a> dyn GenericTrait<'a> + 'a {}

实现 traits 的具体类型能够持有引用,因此类型本身拥有生命周期绑定,自然它的 trait object 也有对应的生命周期绑定。(即使实现 trait 的类型没有引用),还可以专门为引用类型实现 trait,这样 trait object 拥有生命周期绑定就显而易见了。

这里显然在解释为什么Box托管的trait object也有生命周期绑定,为什么不放在开头呢?然后再解释如何推断这些生命周期不是更自然,更容易理解吗?后面一段又说了spawn的例子,又是建立在默认推断 ’static 的知识上。啊这……这一段的行文我觉得有问题。

1
2
3
4
5
6
7
8
9
10
trait Trait {}

struct Struct {}
struct Ref<'a, T>(&'a T);

impl Trait for Struct {}
impl Trait for &Struct {} // 直接对引用类型实现 Trait
// 显然,使用&Struct来制作Box托管的trait object,比如`Box::new(&'a Struct{}) as Box<dyn Trait>`
// 其类型实际是`Box<dyn Trait + 'a>`,不能用到期待Box<dyn Trait + 'static>的地方
impl<'a, T> Trait for Ref<'a, T> {} // 对包含引用类型的结构实现 Trait

(虽然这些规则很繁复、多数时候省略都OK,但是),无论怎样,审视这些规则是值得的,因为在某些场合,新手会被这个问题导致的奇怪错误搞迷糊,比如当他们把函数中的 trait object 重构为 泛型,后者反过来,将 泛型 重构为 trait object,比如下面这个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
use std::fmt::Display;

fn dynamic_thread_print(t: Box<dyn Display + Send>) {
std::thread::spawn(move || {
println!("{}", t);
}).join();
}

fn static_thread_print<T: Display + Send>(t: T) {
std::thread::spawn(move || {
println!("{}", t);
}).join();
}

报错为:

1
2
3
4
5
6
7
8
9
10
11
12
13
error[E0310]: the parameter type `T` may not live long enough
--> src/lib.rs:10:5
|
9 | fn static_thread_print<T: Display + Send>(t: T) {
| -- help: consider adding an explicit lifetime bound...: `T: 'static +`
10 | std::thread::spawn(move || {
| ^^^^^^^^^^^^^^^^^^
|
note: ...so that the type `[closure@src/lib.rs:10:24: 12:6 t:T]` will meet its required lifetime bounds
--> src/lib.rs:10:5
|
10 | std::thread::spawn(move || {
| ^^^^^^^^^^^^^^^^^^

编译器已经告诉我们如修改,那就:

1
2
3
4
5
6
7
8
9
10
11
12
13
use std::fmt::Display;

fn dynamic_thread_print(t: Box<dyn Display + Send>) {
std::thread::spawn(move || {
println!("{}", t);
}).join();
}

fn static_thread_print<T: Display + Send + 'static>(t: T) {
std::thread::spawn(move || {
println!("{}", t);
}).join();
}

编译成功,但是这两个函数有些奇怪,为什么第二个需要'static绑定,而第一个不需要?这就是因为 trait object 的生命周期默认推断,编译器实际上看到的第一个函数有’static绑定。

关键点:

  • 所有的 trait objects 都有一些默认推断的生命周期绑定

7) 编译器错误信息会指明如何修改我的程序

该误解的推论

  • Rust针对 trait objects 的生命周期省略规则总是正确的
  • Rust比我更了解我程序的语义

该误解是前两个误解的结合,这是例子:

1
2
3
4
5
use std::fmt::Display;

fn box_displayable<T: Display>(t: T) -> Box<dyn Display> {
Box::new(t)
}

抛出如下错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
error[E0310]: the parameter type `T` may not live long enough
--> src/lib.rs:4:5
|
3 | fn box_displayable<T: Display>(t: T) -> Box<dyn Display> {
| -- help: consider adding an explicit lifetime bound...: `T: 'static +`
4 | Box::new(t)
| ^^^^^^^^^^^
|
note: ...so that the type `T` will meet its required lifetime bounds
--> src/lib.rs:4:5
|
4 | Box::new(t)
| ^^^^^^^^^^^

这个推荐的修复信息,依据的是Box为 trait object 自动推断出的'static生命周期绑定,但不管怎样,我们先试着按照它的说的修改。

1
2
3
4
5
use std::fmt::Display;

fn box_displayable<T: Display + 'static>(t: T) -> Box<dyn Display> {
Box::new(t)
}

因此程序通过了编译……但这是我们希望的吗?大概是,也可能有问题。虽然编译期没有提及,但是这样修改可能更合适:

1
2
3
4
5
use std::fmt::Display;

fn box_displayable<'a, T: Display + 'a>(t: T) -> Box<dyn Display + 'a> {
Box::new(t)
}

这个函数可以接受的参数兼容上一个版本,并且还支持更多。情况更好了吗?也不一定,取决于我们程序本身的要求和约束。这个例子可能有点抽象,所以我们再看一个更简单的、更明显的例子:

1
2
3
fn return_first(a: &str, b: &str) -> &str {
a
}

报错

1
2
3
4
5
6
7
8
9
10
error[E0106]: missing lifetime specifier
--> src/lib.rs:1:38
|
1 | fn return_first(a: &str, b: &str) -> &str {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `a` or `b`
help: consider introducing a named lifetime parameter
|
1 | fn return_first<'a>(a: &'a str, b: &'a str) -> &'a str {

错误信息推荐为输入引用和输出引用使用相同的标记。虽然能编译了,但是我们我们的返回值的限制可能过强,也许我们需要的是

1
2
3
fn return_first<'a>(a: &'a str, b: &str) -> &'a str {
a
}

关键点:

  • Rust关于 trait objects 的生命周期省略规则并不是对任何情况都适用
  • 在你程序的语义层面,Rust了解得有限,并不及你
  • Rust在错误信息中给出的修改意见虽然可以让程序通过编译,但可能并不是最适合你程序的方案。

8) 生命周期可以在运行时扩张或者缩小

该误解的推论:

  • 容器类型可以在运行时交换(swap)引用,从而改变交换双方的生命周期
  • Rust的借用检查器执行了高级的控制流分析

这段代码不能编译:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct Has<'lifetime> {
lifetime: &'lifetime str,
}

fn main() {
let long = String::from("long");
let mut has = Has { lifetime: &long };
assert_eq!(has.lifetime, "long");

{
let short = String::from("short");
// "切换" 到更短的生命周期
has.lifetime = &short;
assert_eq!(has.lifetime, "short");

// "切换回" 长生命周期
has.lifetime = &long;
assert_eq!(has.lifetime, "long");
// `short` 在这里 dropped
}

// 编译错误,显示 `short` 被drop后仍然被借用
assert_eq!(has.lifetime, "long");
}

抛出错误

1
2
3
4
5
6
7
8
9
10
error[E0597]: `short` does not live long enough
--> src/main.rs:11:24
|
11 | has.lifetime = &short;
| ^^^^^^ borrowed value does not live long enough
...
15 | }
| - `short` dropped here while still borrowed
16 | assert_eq!(has.lifetime, "long");
| --------------------------------- borrow later used here

这个错误是因为Rust发现了替换动作,并选择了最短的生命周期进行绑定,然后静态分析到最后一句println时,发现引用应该在更大的范围内有效,于是报错。如果删除最后一句话,这段代码可以编译:确定的周期是short,long被缩短后使用。

(尝试使用false,让替换分支在运行时不执行,看Rust编译器是否提前知道,从而避免对生命周期的错误判断)

下面这段代码也不能通过编译,报告相同的错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct Has<'lifetime> {
lifetime: &'lifetime str,
}

fn main() {
let long = String::from("long");
let mut has = Has { lifetime: &long };
assert_eq!(has.lifetime, "long");

// 使用false,指明不执行以下block
if false {
let short = String::from("short");
has.lifetime = &short;
assert_eq!(has.lifetime, "short");

has.lifetime = &long;
assert_eq!(has.lifetime, "long");
// `short` 在这里 dropped
}

// 还是编译错误,显示 `short` 被drop后仍然被借用
assert_eq!(has.lifetime, "long");
}

由此我们知道,生命周期参数是在编译期就被静态验证的,并且借用检查器的控制流分析很初级,它假定每个if-else的 block 都可能会执行,每个 match 的 arm 都可能会被选中,从而为(未限制执行生命周期的)变量选择一个最短的生命周期绑定。一旦生命周期被绑定,就永远被绑定了。变量的生命周期只有可能缩短(子类型当作父类型使用),但这种缩短,也是在编译期就被确定的。

关键点:

  • 生命周期在编译时被静态验证
  • 在运行时,变量的生命周期不会以任何形式发生改变
  • Rust的借用检查器假定所有分支都会被命中,为变量选择最短的生命周期。

9) 将可变引用退化为共享引用是安全的

该误解的推论:

  • 重新借用一个引用,会结束被借引用的生命周期并产生一个新的引用

如果函数的一个参数是共享引用,那么你其实可以传递一个可变引用,因为Rust会将可变引用重新借用出一个不可变引用:

1
2
3
4
5
6
7
fn takes_shared_ref(n: &i32) {}

fn main() {
let mut a = 10;
takes_shared_ref(&mut a); // 通过编译
takes_shared_ref(&*(&mut a)); // 将上一行的语法糖去掉后,使用deref重新借出不可变引用
}

直觉上这容易理解,因为把一个可变应用重借出一个不可变引用,不会造成什么危害,对吧?答案是否定的,下面这段程序不能编译

1
2
3
4
5
6
fn main() {
let mut a = 10;
let b: &i32 = &*(&mut a); // re-borrowed as immutable
let c: &i32 = &a;
dbg!(b, c); // compile error
}

抛出错误

1
2
3
4
5
6
7
8
9
error[E0502]: cannot borrow `a` as immutable because it is also borrowed as mutable
--> src/main.rs:4:19
|
3 | let b: &i32 = &*(&mut a);
| -------- mutable borrow occurs here
4 | let c: &i32 = &a;
| ^^ immutable borrow occurs here
5 | dbg!(b, c);
| - mutable borrow later used here

这段代码中,(第三行) 我们的确执行了可变借用,但是立马重借出不可变借用(b),(期望)可变借用自动销毁。但Rust对待重借出的不可变引用(b)时,感觉和原有的可变借用如出一辙(b,c不能同时存活,变相等于可变引用并未销毁)。尽管上例中(即使放开限制,drop掉可变引用)不会出现问题,但是允许将可变引用直接降级为不可变引用(并且销毁(归还)原有的可变引用),确实会造成潜在的内存不安全:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
use std::sync::Mutex;

struct Struct {
mutex: Mutex<String>
}

impl Struct {
// 把 mut self 降级为 shared str
fn get_string(&mut self) -> &str {
self.mutex.get_mut().unwrap()
}
fn mutate_string(&self) {
// 如果 Rust 允许将 可变引用 直接降级为 不可变引用
// 那么下一行代码会使通过`get_string` 方法得到的共享引用失效
*self.mutex.lock().unwrap() = "surprise!".to_owned();
}
}

fn main() {
let mut s = Struct {
mutex: Mutex::new("string".to_owned())
};
let str_ref = s.get_string(); // 可变引用 直接降级为 不可变引用
s.mutate_string(); // str_ref 失效, 成为悬空指针
dbg!(str_ref); // 编译失败
}

这个例子的重点是,当你向可变引用 mut self 重借出共享引用时,你会陷入一个反直觉的陷阱:这个动作实际拓展了 mut self 的生命周期,和被借出的共享应用一样长(编译时确定作用域和生命周期参数),即使 可变借用 本身已经被drop

我一直不是很理解这里的”即使被drop“,既然编译器都已经给可变引用mut self和重借出引用&str附上了相同的生命周期参数,那显然mut self就没有释放啊,作者的思维还是停留在上一个例子里?)

使用重借出引用很麻烦(反直觉),它本身虽然是不可变的,但是却不能和其他不可变引用同时存活。重借出引用有 可变引用 和 不可变引用 的缺点,却没有他们的优点。我认为“向可变引用重借出不可变引用”这一行为,在Rust中属于反模式的行为。对这种反模式保持警惕很重要,当你看到下面这样的代码时,可以轻松地识别到它。

1
2
3
4
5
6
7
8
9
10
11
12
// 降级了,要小心!
fn some_function<T>(some_arg: &mut T) -> &T;

struct Struct;

impl Struct {
// 降级了,要小心!
fn some_method(&mut self) -> &self;

// 降级了,要小心!
fn other_method(&mut self) -> &T;
}

即使你在函数或者方法签名中避免了使用重借出,Rust还是存在隐式的重借出,让你不经意间又遇到这个问题,像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
use std::collections::HashMap;

type PlayerID = i32;

#[derive(Debug, Default)]
struct Player {
score: i32,
}

fn start_game(player_a: PlayerID, player_b: PlayerID, server: &mut HashMap<PlayerID, Player>) {
// 从服务器获取选手信息,不存在时创建并更新选手信息
let player_a: &Player = server.entry(player_a).or_default();
let player_b: &Player = server.entry(player_b).or_default();

// 同时持有两个选手并操作
dbg!(player_a, player_b); // 编译错误
}

出错原因是entry语法中or_default返回的是&mut Player,由于显式的类型标注,发生了隐式的重借出。完成相同的目的,我们需要

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use std::collections::HashMap;

type PlayerID = i32;

#[derive(Debug, Default)]
struct Player {
score: i32,
}

fn start_game(player_a: PlayerID, player_b: PlayerID, server: &mut HashMap<PlayerID, Player>) {
// drop 返回的 Player的可变引用,它们不能同时使用
server.entry(player_a).or_default();
server.entry(player_b).or_default();

// 再次获取选手信息,以不可变引用的形式,没有重借出
let player_a = server.get(&player_a);
let player_b = server.get(&player_b);

dbg!(player_a, player_b); // 成功编译

这有点笨拙和繁复,但也算我们为”内存安全祭坛“献上的祭品吧。

关键点:

  • 尽量不要向可变引用重借出共享引用,否则你会很难受
  • 对一个可变引用重借出,并不会终止它的生命周期(会伴随借出的共享引用,随时备查),即使它被drop

10) 闭包和函数有相同的生命周期省略规则

与其说这是一个误解,不如说它是Rust本身的陷阱。

闭包,尽管(其行为)也是个函数,却不遵守函数的生命周期省略规则。

1
2
3
4
5
6
7
fn function(x: &i32) -> &i32 {
x
}

fn main() {
let closure = |x: &i32| x;
}

报错:

1
2
3
4
5
6
7
8
error: lifetime may not live long enough
--> src/main.rs:6:29
|
6 | let closure = |x: &i32| x;
| - - ^ returning this value requires that `'1` must outlive `'2`
| | |
| | return type of closure is &'2 i32
| let's call the lifetime of this reference `'1`

去掉语法糖后:

1
2
3
4
5
6
7
8
9
10
// 输出引用 沿用 输入引用 的生命周期
fn function<'a>(x: &'a i32) -> &'a i32 {
x
}

fn main() {
// 输入和输出有个各自的生命周期参数
let closure = for<'a, 'b> |x: &'a i32| -> &'b i32 { x };
// 注意: 上面这行有语法错误,但是为了表达意图,姑且任之
}

并没有一个好的理由来解释这种差异的出现。最早闭包实现的时候就用了不同的类型推断语义,现在统一也来不及了,因为这会是一个不向后兼容的修改。所以我们如何显式标注一个闭包的类型?可能的方法有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
fn main() {
// 转换为trait object, 但这是个DST,不能放到栈上,编译错误
let identity: dyn Fn(&i32) -> &i32 = |x: &i32| x;

// 放到堆上,当然可以,但是这有点笨重
let identity: Box<dyn Fn(&i32) -> &i32> = Box::new(|x: &i32| x);

// 也可以跳过堆分配,直接做静态引用
let identity: &dyn Fn(&i32) -> &i32 = &|x: &i32| x;

// 上一行去掉语法糖后:)
let identity: &'static (dyn for<'a> Fn(&'a i32) -> &'a i32 + 'static) = &|x: &i32| -> &i32 { x };

// 要是写法可以换成这样,这把将绝杀,可惜换不得
let identity: impl Fn(&i32) -> &i32 = |x: &i32| x;

// 这样也不错,可还是不行
let identity = for<'a> |x: &'a i32| -> &'a i32 { x };

// impl Trait 的语法可以写在在函数的返回值,所以我们可以引入这样一个工具函数
fn return_identity() -> impl Fn(&i32) -> &i32 {
|x| x
}
let identity = return_identity();

// 工具函数可以写得更泛化一些
fn annotate<T, F>(f: F) -> F where F: Fn(&T) -> &T {
f
}
let identity = annotate(|x: &i32| x);
}

关键点:

  • 每门语言都有陷阱🤷

11) 'static引用总能强制转换为'a引用

之前我已经举过这个例子:

1
2
fn get_str<'a>() -> &'a str; // generic version
fn get_str() -> &'static str; // 'static version

一些读者曾联系我,询问这两种方法是否有现实意义的区别。经过探究,答案是肯定的,他们确实有区别。

通常对值来说,在使用'a绑定引用的地方,我们总可以使用'static绑定引用进行替换,因为Rust会把'static引用强制转换为'a引用。(比如代码期待&'a i32的地方,我们总可以使用&'static i32)。这是符合直觉的,在期待短生命周期引用的地方使用长生命周期引用,并不会产生内存安全问题(长生命周期的引用必然在更小的作用域内保持有效)。下面的代码可以如期编译:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use rand;

fn generic_str_fn<'a>() -> &'a str {
"str"
}

fn static_str_fn() -> &'static str {
"str"
}

fn a_or_b<T>(a: T, b: T) -> T {
if rand::random() {
a
} else {
b
}
}

fn main() {
let some_string = "string".to_owned();
let some_str = &some_string[..];
let str_ref = a_or_b(some_str, generic_str_fn()); // compiles
let str_ref = a_or_b(some_str, static_str_fn()); // compiles
}

然而,当引用属于函数类型签名的一部分时,这种转换不会生效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
use rand;

fn generic_str_fn<'a>() -> &'a str {
"str"
}

fn static_str_fn() -> &'static str {
"str"
}

fn a_or_b_fn<T, F>(a: T, b_fn: F) -> T
where F: Fn() -> T
{
if rand::random() {
a
} else {
b_fn()
}
}

fn main() {
let some_string = "string".to_owned();
let some_str = &some_string[..];
let str_ref = a_or_b_fn(some_str, generic_str_fn); // compiles
let str_ref = a_or_b_fn(some_str, static_str_fn); // compile error
}

抛出这样的错误

1
2
3
4
5
6
7
8
9
10
error[E0597]: `some_string` does not live long enough
--> src/main.rs:23:21
|
23 | let some_str = &some_string[..];
| ^^^^^^^^^^^ borrowed value does not live long enough
...
25 | let str_ref = a_or_b_fn(some_str, static_str_fn);
| ---------------------------------- argument requires that `some_string` is borrowed for `'static`
26 | }
| - `some_string` dropped here while still borrowed

这是否属于 Rust 缺陷还存在争议,毕竟前一个例子中,是在值上做直接转换,从 &'static str&'a str。但是当前例子是在转换类型,从 for<T> Fn() -> &'static Tfor<'a, T> Fn() -> &'a T

关键点:

  • 拥有签名for<'a, T> fn() -> &'a T的函数,比for<T> fn() -> &'static T更加灵活,适应更多的场景

结论

  • T&T&mut的超集
  • &T&mut T不相交

  • T: 'static应该被读作“T'static生命周期绑定”。

  • T: 'static 表示T是一个拥有 'static 生命周期的借用 或者 是一个所有权类型 (原文这里用的就是with 'static,人晕了,不应该是绑定吗? )
  • 由于T: 'static中的T包含所有权类型,也就意味着T

    • 可以在运行时被动态创建
    • 不必对整个程序有效
    • 可以在safe Rust中自由地修改
    • 可以在运行时被释放
    • 可以有自由的生命周期
  • T:'a&'a T 更广泛,更灵活

  • 如果 T: 'static 那么 T: 'a 也成立,因为对任意 'a都有 'static >= 'a ('static 是所有生命周期类型的子类型)
  • 几乎所有Rust代码都是泛型代码,同时到处都有被省略的生命周期标记。
  • Rust关于函数的生命周期省略规则并不是对任何情况都适用
  • 在你程序的语义层面,Rust了解得有限,并不及你
  • Rust专家建议:给生命周期标记取描述性的名字
  • 在显式放置生命周期标记时,多思考为什么
  • Rust在错误信息中给出的修改意见虽然可以让程序通过编译,但可能并不是最适合你程序的方案。
  • 生命周期在编译时被静态验证
  • 在运行时,变量的生命周期不会以任何形式发生改变
  • Rust的借用检查器假定所有分支都会被命中,为变量选择最短的生命周期。

  • 尽量不要向可变引用重借出共享引用,否则你会很难受

  • 对一个可变引用重借出,并不会终止它的生命周期(会伴随借出的共享引用,随时备查),即使它被drop
  • 每门语言都有陷阱🤷
  • 拥有签名for<'a, T> fn() -> &'a T的函数,比for<T> fn() -> &'static T更加灵活,适应更多的场景

讨论

在这些地方讨论这篇文章

获取通知

拓展阅读