【翻译】Rust生命周期常见误区

5月19日, 2020 · 阅读大概需要37分钟 · #rust · #生命周期

目录

介绍

误解列表

1)T只包含所有权类型

2) 如果T: 'static那么T必须在整个程序运行中都是有效的

3)&'a T和T: 'a是相同的

4) 我的代码没用到泛型,也不含生命周期

5) 如果编译能通过,那么我的生命周期标注就是正确的

6) 装箱的trait对象没有生命周期

7) 编译器报错信息会告诉我怎么修改我的代码

8) 生命周期可以在运行时变长缩短

9) 将可变引用降级为共享引用是安全的

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

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

总结

讨论

关注

介绍

我曾经有过的所有这些对生命周期的误解,现在有很多初学者也深陷于此。我用到的术语可能不是标准的,所以下面列了一个表格来解释它们的用意。

误解列表

简而言之:变量的生命周期指的是这个变量所指的数据可以被编译器静态验证的、在当前内存地址有效期的长度。我现在会用大约~8000字来详细地解释一下那些容易误解的地方。

1)T只包含所有权类型

这个误解比起说生命周期,它和泛型更相关,但在Rust中泛型和生命周期是紧密联系在一起的,不可只谈其一。

当我刚开始学习Rust的时候,我理解i32,&i32,和&mut i32是不同的类型,也明白泛型变量T代表着所有可能类型的集合。但尽管这二者分开都懂,当它们结合在一起的时候我却陷入困惑。在我这个Rust初学者的眼中,泛型是这样的运作的:

T包含一切所有权类型;&T包含一切不可变借用类型;&mut T包含一切可变借用类型。T,&T, 和&mut T是不相交的有限集。简洁明了,符合直觉,但却完全错误。下面这才是泛型真正的运作方式:

T,&T, 和&mut T都是无限集, 因为你可以无限借用一个类型。T是&T和&mut T的超集.&T和&mut T是不相交的集合。让我们用几个例子来检验一下这些概念:

trait Trait {}

impl Trait for T {}

impl Trait for &T {} // 编译错误

impl Trait for &mut T {} // 编译错误

上面的代码并不能如愿编译:

error[E0119]: conflicting implementations of trait `Trait` for type `&_`:

--> src/lib.rs:5:1

|

3 | impl Trait for T {}

| ------------------- first implementation here

4 |

5 | impl Trait for &T {}

| ^^^^^^^^^^^^^^^^^^^^ conflicting implementation for `&_`

error[E0119]: conflicting implementations of trait `Trait` for type `&mut _`:

--> src/lib.rs:7:1

|

3 | impl Trait for T {}

| ------------------- first implementation here

...

7 | impl Trait for &mut T {}

| ^^^^^^^^^^^^^^^^^^^^^^^^ conflicting implementation for `&mut _`

编译器不允许我们为&T和&mut T实现Trait,因为这样会与为T实现的Trait冲突,T本身已经包含了所有&T和&mut T。下面的代码能够如愿编译,因为&T和&mut T是不相交的:

trait Trait {}

impl Trait for &T {} // 编译通过

impl Trait for &mut T {} // 编译通过

要点

T是&T和&mut T的超集

&T和&mut T是不相交的集合

2) 如果T: 'static那么T必须在整个程序运行中都是有效的

误解推论

T: 'static应该被看作"T拥有'static生命周期 "

&'static T和T: 'static没有区别

如果T: 'static那么T必须为不可变的

如果T: 'static那么T只能在编译期创建

大部分Rust初学者是从类似下面这个代码示例中接触到'static生命周期的:

fn main() {

let str_literal: &'static str = "str literal";

}

他们被告知"str literal"是硬编码在编译出来的二进制文件中的, 并会在运行时被加载到只读内存,所以必须是不可变的且在整个程序的运行中都是有效的, 这就是它成为'static的原因。而这些观念又进一步被用static关键字来定义静态变量的规则所加强。

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

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

fn main() {

MUT_BYTES[0] = 99; // 编译错误,修改静态变量是unsafe的

unsafe {

MUT_BYTES[0] = 99;

assert_eq!(99, MUT_BYTES[0]);

}

}

认为静态变量

只可以在编译期创建

必须是不可变的,修改它们是unsafe的

在整个程序的运行过程中都是有效的

'static生命周期大概是以静态变量的默认生命周期命名的,对吧?那么有理由认为'static生命周期也应该遵守相同的规则,不是吗?

是的,但拥有'static生命周期的类型与'static约束的类型是不同的。后者能在运行时动态分配,可以安全地、自由地修改,可以被drop, 还可以有任意长度的生命周期。

在这个点,很重要的是要区分&'static T和T: 'static。

&'static T是对某个T的不可变引用,这个引用可以被无限期地持有直到程序结束。这只可能发生在T本身不可变且不会在引用被创建后移动的情况下。T并不需要在编译期就被创建,因为我们可以在运行时动态生成随机数据, 然后以内存泄漏为代价返回'static引用,例如:

use rand;

// 在运行时生成随机&'static str

fn rand_str_generator() -> &'static str {

let rand_string = rand::random::().to_string();

Box::leak(rand_string.into_boxed_str())

}

T: 'static是指T可以被无限期安全地持有直到程序结束。T: 'static包括所有&'static T,此外还包括所有的所有权类型,比如String,Vec等。数据的所有者能够保证数据只要还被持有就不会失效,因此所有者可以无限期安全地持有该数据直到程序结束。T: 'static应该被看作“T受'static生命周期约束”而非“T有着'static生命周期”。这段代码能帮我们阐释这些概念:

use rand;

fn drop_static

std::mem::drop(t);

}

fn main() {

let mut strings: Vec = Vec::new();

for _ in 0..10 {

if rand::random() {

// 所有字符串都是随机生成的

// 并且是在运行时动态申请的

let string = rand::random::().to_string();

strings.push(string);

}

}

// 这些字符串都是所有权类型,所以它们满足'static约束

for mut string in strings {

// 这些字符串都是可以修改的

string.push_str("a mutation");

// 这些字符串都是可以被drop的

drop_static(string); // 编译通过

}

// 这些字符串都在程序结束之前失效

println!("i am the end of the program");

}

要点

T: 'static应该被看作“T受'static生命周期约束”

如果T: 'static那么T可以是有着'static生命周期的借用类型

由于T: 'static包括了所有权类型,这意味着T

可以在运行时动态分配

不一定要在整个程序的运行过程中都有效

可以被安全地、自由地修改

可以在运行时被动态drop掉

可以有不同长度的生命周期

3)&'a T和T: 'a是相同的

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

&'a T不光要求,同时也隐含着T: 'a, 因为如果T本身都不能在'a内有效, 那对T的有'a生命周期的引用也不可能是有效的。例如,Rust编译器从来不会允许创建&'static Ref这个类型,因为如果Ref只在'a内有效,我们不可能弄出一个对它的'static的引用。

T: 'a包括了所有&'a T,但反过来不对。

// 只接受以'a约束的引用类型

fn t_ref(t: &'a T) {}

// 接受所有以'a约束的类型

fn t_bound(t: T) {}

// 包含引用的所有权类型

struct Ref(&'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变量是以'static约束的,也满足'a约束

t_bound(string); // 编译通过

}

要点

T: 'a比起&'a T更泛化也更灵活

T: 'a接受所有权类型、包含引用的所有权类型以及引用

&'a T只接受引用

如果T: 'static那么T: 'a, 因为对于所有'a都有'static>='a

4) 我的代码没用到泛型,也不含生命周期

误解推论

避免使用泛型和生命周期是可能的

这种安慰性的误解的存在是由于Rust的生命周期省略规则, 这些规则让你能够在函数中省略掉生命周期记号, 因为Rust的借用检查器能根据以下规则将它们推导出来:

每个传入的引用都会有一个单独的生命周期

如果只有一个传入的生命周期,那么它将被应用到所有输出的引用上

如果有多个传入的生命周期,但其中一个是&self或者&mut self,那么这个生命周期将会被应用到所有输出的引用上

除此之外的输出的生命周期都必须显示标注出来

如果一时间难以想明白这么多东西,那让我们来看一些例子:

// 省略

fn print(s: &str);

// 展开

fn print

// 省略

fn trim(s: &str) -> &str;

// 展开

fn trim &'a str;

// 不合法,无法确定输出的生命周期,因为没有输入的

fn get_str() -> &str;

// 显式的写法包括

fn get_str

fn get_str() -> &'static str; // 'static 版本

// 不合法,无法确定输出的生命周期,因为有多个输入

fn overlap(s: &str, t: &str) -> &str;

// 显式(但仍有部分省略)的写法包括

fn overlap &'a str; // 输出生命周期不能长于s

fn overlap &'a str; // 输出生命周期不能长于t

fn overlap

fn overlap(s: &str, t: &str) -> &'static str; // 输出生命周期可以长于s和t

fn overlap

// 展开

fn overlap(s: &'a str, t: &'b str) -> &'a str;

fn overlap(s: &'a str, t: &'b str) -> &'b str;

fn overlap

fn overlap(s: &'a str, t: &'b str) -> &'static str;

fn overlap

// 省略

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

// 展开

fn compare(&'a self, &'b str) -> &'a str;

如果你曾写过

结构体方法

接收引用的函数

返回引用的函数

泛型函数

trait object(后面会有更详细的讨论)

闭包(后面会有更详细的讨论)

那么你的代码就有被省略的泛型生命周期记号。

要点

几乎所有Rust代码都是泛型代码,到处都有被省略的生命周期记号

5) 如果编译能通过,那么我的生命周期标注就是正确的

误解推论

Rust对函数的的生命周期省略规则总是正确的

Rust的借用检查器在技术上和语义上总是正确的

Rust比我更了解我的程序的语义

Rust程序是有可能在技术上能通过编译,但语义上仍然是错的。来看一下这个例子:

struct ByteIter

remainder: &'a [u8]

}

impl {

fn next(&mut self) -> Option {

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是在字节切片上迭代的迭代器,为了简洁我们跳过对Iteratortrait的实现。这看起来没什么问题,但如果我们想同时检查多个字节呢?

fn main() {

let mut bytes = ByteIter { remainder: b"1123" };

let byte_1 = bytes.next();

let byte_2 = bytes.next();

if byte_1 == byte_2 {

// 做点什么

}

}

啊哦!编译错误:

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]的时候 我们也会想到将来可能它会被应用到那些拷贝/克隆的代价很昂贵或根本不可能的类型上。噢,我想我们对这没什么办法,代码能过编译,那么生命周期标记必然是对的不是吗?

struct ByteIter

remainder: &'a [u8]

}

impl {

fn next Option

if self.remainder.is_empty() {

None

} else {

let byte = &self.remainder[0];

self.remainder = &self.remainder[1..];

Some(byte)

}

}

}

这一点帮助都没有,我仍然搞不明白。这里有个只有Rust专家才知道的小窍门:给你的生命周期标记取个有描述性的名字。我们再试一次:

struct ByteIter

remainder: &'remainder [u8]

}

impl {

fn next Option

if self.remainder.is_empty() {

None

} else {

let byte = &self.remainder[0];

self.remainder = &self.remainder[1..];

Some(byte)

}

}

}

每个返回的字节都被用'mut_self标记了,但这些字节显然是来自于'remainder的, 让我们来改一下。

struct ByteIter

remainder: &'remainder [u8]

}

impl {

fn next(&mut self) -> Option

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); // 我们甚至可以在这里把迭代器drop掉!

if byte_1 == byte_2 { // 编译通过

// 做点什么

}

}

现在让我们回顾一下,我们前一版的程序显然是错误的,但为什么Rust仍然允许它通过编译呢?答案很简单:这么做是内存安全的。

Rust的借用检查器对程序的生命周期标记只要求到能够以静态的方式验证程序的内存安全。Rust会爽快地编译一个程序,即使它的生命周期标记有语义上的错误, 这带来的结果就是程序会变得过于受限。

来看一个与前一个相反的例子:Rust的生命周期省略规则恰好在这个例子上语义是正确的, 但我们却无意中用了一些多余的显式生命周期标记写了个非常受限的方法。

#[derive(Debug)]

struct NumRef

impl {

// 我的结构体是在'a上泛型的,所以我同样也要

// 标记一下我的self参数,对吗?(答案是:不,不对)

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的生命周期省略规则来处理它:

#[derive(Debug)]

struct NumRef

impl {

// 去掉mut self前面的'a

fn some_method(&mut self) {}

// 上一段代码脱掉语法糖后变为

fn some_method_desugared

}

fn main() {

let mut num_ref = NumRef(&5);

num_ref.some_method();

num_ref.some_method(); // 编译通过

println!("{:?}", num_ref); // 编译通过

}

要点

Rust的函数生命周期省略规则并不总是对所有情况都正确的

Rust对你的程序的语义了解并不比你多

给你的生命周期标记起一个更有描述性的名字

在你使用显式生命周期标记的时候要想清楚它们应该被用在哪以及为什么要这么用

6) 装箱的trait对象没有生命周期

早前我们讨论了Rust对函数的生命周期省略规则。Rust同样有着对于trait对象的生命周期省略规则,它们是:

如果一个trait对象作为一个类型参数传递到泛型中,那么它的生命约束会从它包含的类型中推断

如果包含的类型中有唯一的约束,那么就使用这个约束。

如果包含的类型中有超过一个约束,那么必须显式指定约束。

如果以上都不适用,那么:

如果trait是以单个生命周期约束定义的,那么就使用这个约束

如果所有生命周期约束都是'static的,那么就使用'static作为约束

如果trait没有生命周期约束,那么它的生命周期将会从表达式中推断,如果不在表达式中,那么就是'static的

这么多东西听起来超级复杂,但我们可以简单地总结为"trait对象的生命周期约束是从上下文中推断出来的。"在我们看过几个例子后,我们会发现生命周期约束推断其实是很符合直觉的,我们不需要去记这些很正式的规则。

use std::cell::Ref;

trait Trait {}

// 省略

type T1 = Box;

// 展开,Box对T没有生命周期约束,所以被推断为'static

type T2 = Box

// 省略

impl dyn Trait {}

// 展开

impl dyn Trait + 'static {}

// 省略

type T3

// 展开, 因为&'a T 要求 T: 'a, 所以推断为 'a

type T4

// 省略

type T5;

// 展开, 因为Ref

type T6

trait GenericTrait

// 省略

type T7>;

// 展开

type T8 + 'a>;

// 省略

impl {}

// 展开

impl + 'a {}

实现了某个trait的具体的类型可以包含引用,因此它们同样拥有生命周期约束,且对应的trait对象也有生命周期约束。你也可以直接为引用实现trait,而引用显然有生命周期约束。

trait Trait {}

struct Struct {}

struct Ref

impl Trait for Struct {}

impl Trait for &Struct {} // 直接在引用类型上实现Trait

impl {} // 在包含引用的类型上实现Trait

不管怎样,这都值得我们仔细研究,因为新手们经常在将一个使用trait对象的函数重构成使用泛型的函数(或者反过来)的时候感到困惑。我们来看看这个例子:

use std::fmt::Display;

fn dynamic_thread_print(t: Box) {

std::thread::spawn(move || {

println!("{}", t);

}).join();

}

fn static_thread_print(t: T) {

std::thread::spawn(move || {

println!("{}", t);

}).join();

}

这会抛出下面的编译错误:

error[E0310]: the parameter type `T` may not live long enough

--> src/lib.rs:10:5

|

9 | fn static_thread_print(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 || {

| ^^^^^^^^^^^^^^^^^^

很好,编译器告诉了我们怎么解决这个问题,我们来试试。

use std::fmt::Display;

fn dynamic_thread_print(t: Box) {

std::thread::spawn(move || {

println!("{}", t);

}).join();

}

fn static_thread_print

std::thread::spawn(move || {

println!("{}", t);

}).join();

}

编译通过,但这两个函数放在一块儿看起来有点怪,为什么第二个函数对T有'static约束,而第一个没有?这个问题很刁钻。根据生命周期省略规则,Rust自动为第一个函数推断出'static约束,所以两个函数实际上都有'static约束。在Rust编译器的眼中是这样的:

use std::fmt::Display;

fn dynamic_thread_print(t: Box

std::thread::spawn(move || {

println!("{}", t);

}).join();

}

fn static_thread_print

std::thread::spawn(move || {

println!("{}", t);

}).join();

}

要点

所有trait对象都有着默认推断的生命周期约束

7) 编译器报错信息会告诉我怎么修改我的代码

误解推论

Rust编译器对于trait objects的生命周期省略规则总是对的

Rust编译器比我更懂我代码的语义

这个误解是前两个误解的合二为一的例子:

use std::fmt::Display;

fn box_displayable(t: T) -> Box {

Box::new(t)

}

抛出如下错误:

error[E0310]: the parameter type `T` may not live long enough

--> src/lib.rs:4:5

|

3 | fn box_displayable(t: T) -> Box {

| -- 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 object推断了一个'static的生命周期约束。

use std::fmt::Display;

fn box_displayable

Box::new(t)

}

现在编译通过了,但这真的是我们想要的吗?可能是,也可能不是,编译去并没有告诉我们其它解决方法 但这个也许合适。

use std::fmt::Display;

fn box_displayable(t: T) -> Box

Box::new(t)

}

这个函数接收的参数和前一个版本一样,但多了不少东西。这样写能让它更好吗?不一定, 这取决于我们的程序的要求和约束。这个例子有些抽象,让我们来看看更简单明了的情况。

fn return_first(a: &str, b: &str) -> &str {

a

}

报错:

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

| ^^^^ ^^^^^^^ ^^^^^^^ ^^^

这个错误信息建议我们给输入和输出打上相同的生命周期标记。这么做虽然能使得编译通过,但却过度限制了返回类型。我们真正想要的是这个:

fn return_first &'a str {

a

}

要点

Rust对trait object的生命周期省略规则并不是在所有情况下都正确。

Rust不见得比你更懂你代码的语义。

Rust编译错误信息给出的修改建议可能能让你的代码编译通过,但这不一定是最符合你的要求的。

8) 生命周期可以在运行时变长缩短

误解推论

容器类型可以通过更换引用在运行时更改自己的生命周期

Rust的借用检查会进行深入的控制流分析

这过不了编译:

struct Has

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`在这里析构

}

// 编译错误,`short`在析构后仍处于借用状态

assert_eq!(has.lifetime, "long");

}

报错:

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

下面这个代码同样过不了编译,报的错和上面一样。

struct Has

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

// 换成短生命周期

has.lifetime = &short;

assert_eq!(has.lifetime, "short");

// 换回长生命周期(并不行)

has.lifetime = &long;

assert_eq!(has.lifetime, "long");

// `short`在这里析构

}

// 仍旧编译错误,`short`在析构后仍处于借用状态

assert_eq!(has.lifetime, "long");

}

生命周期只会在编译期被静态验证,并且Rust的借用检查只能做到基本的控制流分析, 它假设每个if-else中的代码块和match的每个分支都会被执行, 并且其中的每一个变量都能被指定一个最短的生命周期。一旦变量被指定了一个生命周期,它就一直受到这个生命周期约束。变量的生命周期只能缩短, 并且所有缩短都会在编译器被确定。

要点

生命周期是在编译期静态验证的

生命周期不能在运行时变长、缩短或者改变

Rust的借用检查总是会为所有变量指定一个最短可能的生命周期,并且假定所有代码路径都会被执行

9) 将可变引用降级为共享引用是安全的

误解推论

重新借用一个引用会终止它的生命周期并且开始一个新的

你可以向一个接收共享引用的函数传递一个可变引用,因为Rust会隐式将可变引用重新借用为不可变引用:

fn takes_shared_ref(n: &i32) {}

fn main() {

let mut a = 10;

takes_shared_ref(&mut a); // 编译通过

takes_shared_ref(&*(&mut a)); // 上一行的显式写法

}

直觉上这没问题,将一个可变引用重新借用为不可变引用,应该不会有什么害处不是吗?然而并非如此,下面的代码过不了编译。

fn main() {

let mut a = 10;

let b: &i32 = &*(&mut a); // 重新借用为不可变

let c: &i32 = &a;

dbg!(b, c); // 编译错误

}

报错:

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会认为这个不可变的重新借用仍具有可变引用的独占生命周期?虽然上面这个例子没什么问题,但允许将可变引用降级为共享引用实际上引入了潜在的内存安全问题。

use std::sync::Mutex;

struct Struct {

mutex: Mutex

}

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的反模式(anti-pattern)。对这种反模式保持警惕很重要,这可以让你在看到下面这样的代码的时候更容易发现它:

// 将T的可变引用降级为共享引用

fn some_function(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仍然会自动隐式重新借用, 所以很容易无意中遇到这样的问题:

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

// 从服务器中获取player,如果不存在则创建并插入一个新的

let player_a: &Player = server.entry(player_a).or_default();

let player_b: &Player = server.entry(player_b).or_default();

// 用player做点什么

dbg!(player_a, player_b); // 编译错误

}

上面的代码编译失败。因为or_default()返回一个&mut Player, 而我们的显式类型标注&Player使得这个&mut Player被隐式重新借用为&Player。为了通过编译,我们不得不这样写:

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

// 因为我们不能把它们放在一起用,所以这里把返回的Player可变引用析构掉

server.entry(player_a).or_default();

server.entry(player_b).or_default();

// 再次获取这些Player,这次以不可变的方式,避免出现隐式重新借用

let player_a = server.get(&player_a);

let player_b = server.get(&player_b);

// 用Player做点什么

dbg!(player_a, player_b); // 编译通过

}

虽然有点尴尬和笨重,但这也算是为内存安全做出的牺牲。

要点

尽量不要把可变引用重新借用为共享引用,不然你会遇到不少麻烦

重新借用一个可变引用不会使得它的生命周期终结,即使这个可变引用已经析构

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

比起误解,这更像是Rust的一个小陷阱。

闭包,虽然也是个函数,但是它并不遵循和函数相同的生命周期省略规则。

fn function(x: &i32) -> &i32 {

x

}

fn main() {

let closure = |x: &i32| x;

}

报错:

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`

去掉语法糖后:

// 输入的生命周期应用到输出上

fn function &'a i32 {

x

}

fn main() {

// 输入和输出有它们自己独有的生命周期

let closure = for |x: &'a i32| -> &'b i32 { x };

// 注意:上面这行代码不是合法的语法,但可以表达出我们的意思

}

出现这种差异并没有一个好的理由。闭包最早的实现用的类型推断语义和函数不同, 现在变得没法改了,因为将它们统一起来会造成一个不兼容的改动。那么我们要怎么样显式标注闭包的类型呢?我们可选的办法有:

fn main() {

// 转成trait object,变成不定长类型,编译错误

let identity: dyn Fn(&i32) -> &i32 = |x: &i32| x;

// 可以通过将它分配在堆上来绕过这个错误,但这样很笨重

let identity: Box &i32> = Box::new(|x: &i32| x);

// 也可以跳过分配,直接创建一个静态的引用

let identity: &dyn Fn(&i32) -> &i32 = &|x: &i32| x;

// 上一行去掉语法糖之后:)

let identity: &'static (dyn for &'a i32 + 'static) = &|x: &i32| -> &i32 { x };

// 理想中的写法是这样的,但这不是有效的语法

let identity: impl Fn(&i32) -> &i32 = |x: &i32| x;

// 这样也不错,但也不是有效的语法

let identity = for &'a i32 { x };

// "impl trait"可以写在函数返回的位置,我们也可以这样写

fn return_identity() -> impl Fn(&i32) -> &i32 {

|x| x

}

let identity = return_identity();

// 前一种解决方案更泛化的写法

fn annotate(f: F) -> F where F: Fn(&T) -> &T {

f

}

let identity = annotate(|x: &i32| x);

}

相信你已经注意到,在上面的例子中,当闭包类型使用trait约束的时候会遵循一般函数的生命周期省略规则。

这里没有什么真正的教训和洞察,只是它就是这样的而已。

要点

每一门语言都有自己的小陷阱

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

我前面给出了这个例子:

fn get_str

fn get_str() -> &'static str; // 'static版本

有的读者问我这两个在实践中是否有区别。一开始我也不太确定,但不幸的是, 在经过一段时间的研究之后我发现它们在实践中确实存在着区别。

所以一般来说,在操作值得时候我们可以使用'static引用来替换'a引用, 因为Rust会自动将'static引用强制转换到'a引用。直觉上来讲,这没毛病,在一个要求较短生命周期引用的地方使用一个有着更长的生命周期的引用不会造成内存安全问题。下面的代码和我们想的一样编译通过:

use rand;

fn generic_str_fn

"str"

}

fn static_str_fn() -> &'static str {

"str"

}

fn a_or_b(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()); // 编译通过

let str_ref = a_or_b(some_str, static_str_fn()); // 编译通过

}

然而,这种强制转换并不会在引用作为函数的类型签名的一部分的时候出现,所以下面这段代码无法通过编译:

use rand;

fn generic_str_fn

"str"

}

fn static_str_fn() -> &'static str {

"str"

}

fn a_or_b_fn(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); // 编译通过

let str_ref = a_or_b_fn(some_str, static_str_fn); // 编译错误

}

报错:

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 Fn() -> &'static T到for这种更复杂的情况。前者是值之间的强制转换,后者是类型之间的强制转换。

要点

签名为for的函数要比签名为for fn() -> &'static T的函数更为灵活,并且能用在更多场景下

总结

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,因为对于所有'a都有'static>='a

几乎所有Rust代码都是泛型的,到处都有省略的生命周期

Rust的生命周期省略规则并不是在任何情况下都对

Rust并不比你更了解你程序的语义

给生命周期标记起一个有描述性的名字

考虑清楚哪里需要显式写出生命周期标记,以及为什么要这么写

所有trait object都有默认推断的生命周期约束

Rust的编译错误信息可以让你的代码通过编译,但不一定是最符合你代码要求的

生命周期是在编译期静态验证的

生命周期不会以任何方式在运行时变长缩短

Rust的借用检查总会为每个变量选择一个最短可能的生命周期,并且假定每条代码路径都会被执行

尽量避免将可变引用重新借用为不可变引用,不然你会遇到不少麻烦

重新借用一个可变引用不会终止它的生命周期,即使这个可变引用已经析构

每个语言都有自己的小陷阱

讨论

在这些地方讨论这篇文章

learnrust subreddit

official Rust users forum

Twitter

rust subreddit

Hackernews

关注

在Twitter上关注pretzelhammer 来获取最新的博客的更新!

拓展阅读

Learning Rust in 2020

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20200725A0SBSD00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 yunjia_community@tencent.com 删除。

扫码关注云+社区

领取腾讯云代金券