对Rust生命周期的一些常见误解

目录

概述

文中列出的这些误解都是我曾经以为正确的概念,我也看到现在仍有许多初学者都因为这些误解而苦苦挣扎。文中的一些术语可能不标准,我列了一张表,以解释我在文中使用某些短语时原本想表达的意思。

短语 解释
T 1) 包含所有可能类型的集合
2) 该集合中的某种类型
自有类型 某些非引用类型,如i32StringVec
1) 借用类型
2) 引用类型
某些引用类型,可变或不可变均可,如&i32&mut i32
1) 可变借用
2) 互斥借用
排他可变引用,如&mut T
1) immut ref or
2) shared ref
shared immutable reference, i.e. &T

概念误解

总的来说,一个变量的生命周期是指:该变量指向的数据在其当前的内存地址中,能被编译器静态验证为有效的时间。下面将用6500字的篇幅来详细说明大家通常会混淆的地方。

1) T只包含自有类型

这种误解更多的是关于泛型,而是不关于生命周期,但是泛型和生命周期在Rust中紧密的交织在一起,所以不可能在谈论一个的时候不提及另一个。总之:

当我刚开始学习Rust时,我明白i32&i32&mut i32是不同的类型。我也明白一些泛型变量T代表了一个包含所有可能类型的集合。然而,尽管我分别理解了这两个概念,但我并不能把它们放在一起理解。在我的新手Rust大脑中,我认为泛型是这样工作的:

类型变量 T &T &mut T
示例 i32 &i32 &mut i32

T包含所有自有类型。&T包含所有不可变借用类型。&mut T包含所有可变借用类型。T&T&mut T是互不相交的有限集合很好,简单,干净,容易,直观,但这是完全错误的理解。Rust中的泛型实际上是这样工作的:

类型变量 T &T &mut T
示例 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的超集。而&T&mut T是互斥集合。下面用几个例子验证这些概念:

1
2
3
4
5
6
7
trait Trait {}

impl<T> Trait for T {}

impl<T> Trait for &T {} // ❌

impl<T> Trait for &mut T {} // ❌

上面的程序并不能通过编译:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
error[E0119]: conflicting implementations of trait `Trait` for type `&_`:
--> src/lib.rs:5:1
|
3 | impl<T> Trait for T {}
| ------------------- first implementation here
4 |
5 | impl<T> Trait for &T {}
| ^^^^^^^^^^^^^^^^^^^^ conflicting implementation for `&_`

error[E0119]: conflicting implementations of trait `Trait` for type `&mut _`:
--> src/lib.rs:7:1
|
3 | impl<T> Trait for T {}
| ------------------- first implementation here
...
7 | impl<T> Trait for &mut T {}
| ^^^^^^^^^^^^^^^^^^^^^^^^ conflicting implementation for `&mut _`

编译器不允许我们为&T&mut T实现Trait,因为这会与TTrait实现冲突,后者已经包括了&T&mut T的所有内容。下面的程序可以通过编译,因为&T&mut T互斥:

1
2
3
4
5
trait Trait {}

impl<T> Trait for &T {} // ✅

impl<T> Trait for &mut T {} // ✅

主要结论

  • T&T&mut T的超集
  • &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关键字定义static变量的规则得到了进一步加强。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 注意:这个理智纯粹是为了说明问题。
// 永远不要使用`static mut`这种别扭的变量。
// 在Rust中,有一些安全的全局可变单例模式,
// 但这些不在本文的讨论范围之内。

static BYTES: [u8; 3] = [1, 2, 3];
static mut MUT_BYTES: [u8; 3] = [1, 2, 3];

fn main() {
MUT_BYTES[0] = 99; // ❌ - 修改静态变量是非安全的

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

关于static变量

  • 只能在编译期期创建
  • 应该是不可变的,修改静态变量是非安全的
  • 在整个程序的生命周期中都有效

'static生命周期可能是根据static变量的默认生命周期来命名的,对吧?所以'static生命周期遵循static变量相同的规则是很合理的,对吧?

也对,但是具有'static生命周期的类型与受到'static生命周期约束的类型是不同的。后者可以在运行时动态分配、可以安全的在任何地方修改、可以被析构,而且其生命周期可以持续任意的时间。

在这一点上,将&'static TT: 'static区分开很重要。

&'static T是对某个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是一种可以安全的无限期持有的T,包括持有到程序结束。T: 'static包括所有的&'static T,此外还包括所有的自有类型,比如StringVec等。一些数据的持有者被保证,只要所有者持有数据,数据就不会失效,因此所有者可以安全地无限期地持有数据,包括持有到程序结束。T: 'static应该读作“T受到'static生命周期约束”,而不是“T具有'static生命周期”。用一个程序来帮助解释这一概念:

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() {
// 所以字符串都是随机生成且在运行时动态分配的
let string = rand::random::<u64>().to_string();
strings.push(string);
}
}

// 字符串为自有类型,因此受到'static约束
for mut string in strings {
// 所有字符串均可变
string.push_str("a mutation");
// 所有字符串均可析构
drop_static(string);
}

// 所有字符串在程序结束前均已失效
println!("I am the end of program");
}

主要结论

  • T: 'static应读作“_T受到'static生命周期约束_”
  • 如果有T: 'static,则T可以是一个具有'static生命周期的借用类型 一个自有类型
  • 因为T: 'static包括自有类型,这意味着T
    • 可以在运行时被动态的分配
    • 不需要在程序的生命周期中始终有效
    • 可以安全、自由的修改
    • 可以在运行时动态的析构
    • 可以有不同持续时间的生命周期

3) &'a TT: 'a是一回事

这条误解是上一条误解的泛化版本。

&'a T要求并暗指T: 'a,因为如果T本身在生命周期'a上是无效的,那么以'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
24
// 只接受受到'a约束的类型的引用
fn t_ref<'a, T: 'a>(t: &'a T) {}

// 接受任意受到'a约束的类型
fn t_bound<'a, T: 'a>(t: T) {}

// 包含一个引用的自有类型
struct Ref<'a, T: 'a>(&'a T);

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

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

t_ref(&string); // ✅
t_ref(Ref(&string)); // ❌ - 期待收到一个引用,但实际收到一个结构体
t_ref(&Ref(&string)); // ✅

// string var is bounded by 'static which is bounded by 'a
// 字符串变量受到'static约束,即受到'a约束
t_bound(string); // ✅
}

主要结论

  • T: 'a&'a T更通用也更灵活
  • T: 'a接受自有类型、包含引用的自有类型以及引用
  • &'a T只接受引用
  • 如果有T: 'static,那么T: 'a,因为'static >= 所有的'a

4) 我的代码没有泛型也没有生命周期

误解的推论

  • 可以避免使用泛型和生命周期

这一令人欣慰的误解之所以能够继续存在,要感谢Rust的生命周期省略规则,它允许我们在函数中省略生命周期注释,因为Rust的借用检查器会按照这些规则来推断生命周期:

  • 对于一个函数,每个输入的引用参数都具有不同的生命周期
  • 如果有且只有一个输入的生命周期,则所有输出的引用都将应用该生命周期
  • 如果有多个输入的生命周期,但其中一个是&self&mut self,则所有输出的引用都将应用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; // 输出的有效期不能超过s和t
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对象(后文会有更多介绍)
  • 一个闭包(后文会有更多介绍)

那么你的代码中就充满了省略的生命周期注释。

主要结论

  • 几乎所有的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());
}

ByteIter是一个迭代字节片的迭代器。为了简洁起见,我们跳过了Iterator trait实现。它似乎能够正常工作,但如果我们想同时检查几个字节会怎样?

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

我想我们可以复制每个字节。当我们处理字节时复制当然是可以的,但是如果我们将ByteIter变成一个通用的切片迭代器,它可以迭代任何&'a [T],然后我们可能希望在未来使用它来处理那些复制/克隆成本很高,甚至是不可能复制的类型。好吧,我想我们对此无能为力。所以代码能通过编译,生命周期注释就一定正确吗?

不,当前的生命周期注释实际上是错误的来源!它特别难以发现,因为错误的生命周期注释被省略了。让我们声明省略的生命周期以更清楚地了解问题:

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)
}
}
}

每个返回的字节都注释为'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会让它通过编译呢?答案很简单:它是内存安全的。

Rust借用检查器只关心程序中的生命周期注释,它仅使用这些注释来对程序的内存安全性进行静态验证。即使生命周期注释存在语义错误,Rust仍会愉快地编译程序,其结果是使程序受到不必要的限制。

下面是一个与上面相反的简单示例:Rust的生命周期省略规则在这个实例中恰好是语义正确的,但我们无意中编写了一个非常严格的方法,其中包含我们显式声明的不必要的生命周期注释。

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泛型,这是否意味着我需要将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,然后该结构体将成为永久的可变借用,并因此无法再次使用。这种用例非常少见,但困惑的初学者很可能写出上面这种代码,而且还能通过编译。解决方法是不添加不必要的显式生命周期注释,交给Rust的生命周期省略规则处理:

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

impl<'a> NumRef<'a> {
// 不再给mut self添加'a注释
fn some_method(&mut self) {}

// 上面的方法实际上是:
fn some_method_desugared<'b>(&'b mut self){}
}

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

主要结论

  • Rust函数的生命周期省略规则并不适用于所有情况
  • Rust并不比你更了解你程序的语义
  • 给你的生命周期注解起描述性的名字
  • 注意显式生命生命周期注释的位置以及原因

6) box指针中的trait对象没有生命周期概念

之前我们讨论了Rust的函数生命周期省略规则,当然,Rust也有针对特征对象的省略规则,它们是:

  • 如果一个trait对象被用作泛型类型的类型参数,那么它的生命周期约束是从包含的类型中推断出来的
    • 如果包含的类型有唯一的生命周期约束,那么就使用它
    • 如果包含的类型有多个生命周期约束,那么必须指定一个显式生命周期约束
  • 如果以上均不适用,则
    • 如果该trait由唯一的一个生命周期约束定义,则使用该约束
    • 如果'static用于任何生命周期约束,则使用'static
    • 如果trait没有生命周期约束,那么它的生命周期是通过表达式推断出来的,并且在表达式之外是'static

虽然上面这些规则听起来非常复杂,但是可以简单的概括为“trait对象的生命周期约束是从上下文中推断出来的”。在查看了一些示例之后,我们将意识到生命周期约束的推断是非常直观的,因此我们无需记住上面这些形式化的规则:

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
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,因此推断为'a
type T6<'a> = Ref<'a, dyn Trait + 'a>;

trait GenericTrait<'a>: 'a {}

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

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

实现trait的具体类型可以有引用,因此它们也有生命周期约束,所以它们对应的trait对象也具有生命周期约束。你也可以直接为显然具有生命周期约束的引用实现trait:

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

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

impl Trait for Struct {}
impl Trait for &Struct {} // 直接为引用类型实现Trait
impl<'a, T> Trait for Ref<'a, T> {} // 为包含引用的类型实现

无论如何,这值得一读,因为当初学者将函数从使用trait对象重构为泛型,或从泛型到使用trait对象时,常常会感到困惑。以这个程序为例:

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约束的T,而第一个函数就不需要?这是一个棘手的问题。应用生命周期省略规则,Rust会自动在第一个函数中推断出'static约束,因此两者实际上都有 'static约束。这是Rust编译器实际看到的:

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 + 'static>) {
std::thread::spawn(move || {
println!("{}", t);
}).join();
}

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

主要结论

  • 所有trait对象都具有某些推断的默认生命周期约束

7) 编译器的错误提示会告诉我如何修复程序

误解的推论

  • Rust对trait对象的生命周期省略规则总是正确的
  • 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)
| ^^^^^^^^^^^

让我们按照编译器告诉我们方式来修复问题,它将我们boxed trait的约束自动推断为'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
11
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对象的生命周期省略规则并不总是适用于所有情况
  • Rust并不比你更了解你程序的语义
  • Rust编译器的错误消息修复建议能够使你的程序通过编译,但这不一定是使你的程序通过编译的最合适的修复方式

8) 生命周期可以在运行时增长和收缩

误解的推论

  • 容器类型可以在运行时切换引用,以改变它们的生命周期
  • 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");

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

// “切换回”long生命周期(但实际上并没有)
has.lifetime = &long;
assert_eq!(has.lifetime, "long");
// `short`在此处析构
}

assert_eq!(has.lifetime, "long"); // ❌ - `short`在析构之后仍“被借用”
}

这段代码抛出:

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

下面这段代码也不能通过编译,且与上面的代码抛出的错误完全一致:

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");

// 虽然这个代码块从不执行
if false {
let short = String::from("short");
// “切换”到short生命周期
has.lifetime = &short;
assert_eq!(has.lifetime, "short");

// “切换回”long生命周期(但实际上并没有)
has.lifetime = &long;
assert_eq!(has.lifetime, "long");
// `short`在此处析构
}

assert_eq!(has.lifetime, "long"); // ❌ - `short`在析构之后仍“被借用”
}

Rust中的生命周期必须在编译时静态验证,借用检查器只会进行最基本的控制流分析,所以它假设if-else语句中的每个块和match语句中的每个匹配分支都可以被执行,然后为变量选择可能的最短的生命周期。一旦一个变量被一个生命周期约束,就将永远受到该生命周期的约束。变量的生命周期只能收缩,而所有的收缩都是在编译时决定的。

主要结论

  • 生命周期会在编译期静态验证
  • 生命周期不能在运行时以任何方式增长、收缩或改变
  • 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)); // 上一行实际上执行的代码
}

直觉上这是有道理的,因为将一个可变引用重新借用为不可变的应该不会造成什么影响,对吧?令人惊讶的是,实际上并非没有影响,来看下面的程序就无法通过编译:

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); // ❌
}

这段代码抛出如下错误:

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

我们确实进行了可变借用,但它会立即无条件地重新进行不可变借用,然后被丢弃。为什么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
25
26
use std::sync::Mutex;

struct Struct {
mutex: Mutex<String>
}

impl Struct {
// 将可变self降级为共享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); // ❌ - 如我们所料
}

这里的要点是,当你将可变引用重新借用为共享引用时,你不只得到了共享引用还掉进了一个大坑:重新借用还延长了可变引用的生命周期,即使可变引用自身已被析构。使用重新借用的共享引用非常难,因为它不仅不可变,还不能被其他任何共享引用共享。重新借用的共享引用集合了可变引用和共享引用的所有缺点,且并没有得到两者的任一的优点。我认为将可变引用重新借用为共享引用应该被视为Rust反面模式。意识到这种反面模式很重要,这样当您看到这样的代码时就可以很容易地发现它:

1
2
3
4
5
6
7
8
9
10
11
12
// 将可变T降级为共享T
fn some_function<T>(some_arg: &mut T) -> &T;

struct Struct;

impl Struct {
// 将可变self降级为共享self
fn some_method(&mut self) -> &Self;

// 将可变self降级为共享T
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();

// do something with players
dbg!(player_a, player_b); // ❌
}

上面的代码无法通过编译。or_default()将返回一个&mut Player,我们的显式类型注释将使Rust隐式地重新借用为&Player。所以,正确的写法应该是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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>) {
// 丢弃返回的可变Player引用,反正我们也不能同时使用它们
server.entry(player_a).or_default();
server.entry(player_b).or_default();

// 重新获取players,这次直接获取其不可变引用,且不带有啊任何隐式重新借用
let player_a = server.get(&player_a);
let player_b = server.get(&player_b);

// do something with players
dbg!(player_a, player_b); // ✅
}

这么写有点奇怪还很笨拙,但这是我们在内存安全问题上必要的牺牲。

主要结论

  • 尽量不要将可变引用重新借用为共享引用,否则你可能会遇到大麻烦
  • 对可变引用进行重新借用并不会结束它的生命周期,即使该引用已被删除

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对象,占用空间会变得固定,于是产生编译错误
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);
}

你应该已经从上面的示例中注意到了,当闭包类型用作trait约束时,它们确实应用了通常的函数生命周期省略规则。

此处并没有什么教训或启发,Rust现在就是这样。

主要结论

  • 每个编程语言都有些陷阱🤷

结论

  • T&T&mut T的超集
  • &T&mut T是互斥集合
  • T: 'static应读作“_T受到'static生命周期约束_”
  • 如果有T: 'static,则T可以是一个具有'static生命周期的借用类型 一个自有类型
  • 因为T: 'static包括自有类型,这意味着T
    • 可以在运行时被动态的分配
    • 不需要在程序的生命周期中始终有效
    • 可以安全、自由的修改
    • 可以在运行时动态的析构
    • 可以有不同持续时间的生命周期
  • T: 'a&'a T更通用也更灵活
  • T: 'a接受自有类型、包含引用的自有类型以及引用
  • &'a T只接受引用
  • 如果有T: 'static,那么T: 'a,因为'static >= 所有的'a
  • 几乎所有的Rust代码都是泛型代码,到处都有被省略的生命周期注释。
  • Rust函数的生命周期省略规则并不适用于所有情况
  • Rust并不比你更了解你程序的语义
  • 给你的生命周期注解起描述性的名字
  • 注意显式生命生命周期注释的位置以及原因
  • 所有trait对象都具有某些推断的默认生命周期约束
  • Rust编译器的错误消息修复建议能够使你的程序通过编译,但这不一定是使你的程序通过编译的最合适的修复方式
  • 生命周期会在编译期静态验证
  • 生命周期不能在运行时以任何方式增长、收缩或改变
  • Rust借用检查器将始终为变量选择尽可能短的生命周期,并会假设所有分支代码都能够被执行
  • 尽量不要将可变引用重新借用为共享引用,否则你可能会遇到大麻烦
  • 对可变引用进行重新借用并不会结束它的生命周期,即使该引用已被删除
  • 每个编程语言都有些陷阱🤷

讨论

讨论这篇文章的地方:

订阅

订阅下一篇文章的地方:

深入阅读

评论

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×