Rust生命周期常见误区
转载自
中文译者,以及原文,根据约定,本文中所有代码都使用 Apache License Version 2.0 或 MIT License
目录
介绍
我曾经有过的所有这些对生命周期的误解,现在有很多初学者也深陷于此。
我用到的术语可能不是标准的,所以下面列了一个表格来解释它们的用意。
短语 | 意为 |
---|---|
T | 包含了所有可能类型的集合 或 这个集合中的类型 |
所有权类型 | 不含引用的类型, 例如 i32 , String , Vec , 等 |
借用类型 或 引用类型 | 不考虑可变性的引用类型, 例如 &i32 , &mut i32 , 等 |
可变引用 或 独占引用 | 独占的可变引用, 即 &mut T |
不可变引用 或 共享引用 | 共享的不可变引用, 即 &T |
误解列表
简而言之:变量的生命周期指的是这个变量所指的数据可以被编译器静态验证的、在当前内存地址有效期的长度。
我现在会用大约~8000 字来详细地解释一下那些容易误解的地方。
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
是不相交的有限集。 简洁明了,符合直觉,但却完全错误。
下面这才是泛型真正的运作方式:
类型变量 | 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
是不相交的集合。
让我们用几个例子来检验一下这些概念:
trait Trait {}
impl<T> Trait for T {}
impl<T> Trait for &T {} // 编译错误
impl<T> Trait for &mut T {} // 编译错误
上面的代码并不能如愿编译:
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
,因为这样会与为T
实现的Trait
冲突,T
本身已经包含了所有&T
和&mut T
。下面的代码能够如愿编译,因为&T
和&mut T
是不相交的:
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 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::<u64>().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<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的
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<'a, T>
这个类型,因为如果Ref
只在'a
内有效,我们不可能弄出一个对它的'static
的引用。
T: 'a
包括了所有&'a T
,但反过来不对。
// 只接受以'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变量是以'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<'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 object(后面会有更详细的讨论)
- 闭包(后面会有更详细的讨论)
那么你的代码就有被省略的泛型生命周期记号。
要点
- 几乎所有 Rust 代码都是泛型代码,到处都有被省略的生命周期记号
5) 如果编译能通过,那么我的生命周期标注就是正确的
误解推论
- Rust 对函数的的生命周期省略规则总是正确的
- Rust 的借用检查器在技术上和语义上总是正确的
- Rust 比我更了解我的程序的语义
Rust 程序是有可能在技术上能通过编译,但语义上仍然是错的。来看一下这个例子:
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 的实现。
这看起来没什么问题,但如果我们想同时检查多个字节呢?
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<'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 专家才知道的小窍门:
给你的生命周期标记取个有描述性的名字。我们再试一次:
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
的,
让我们来改一下。
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); // 我们甚至可以在这里把迭代器drop掉!
if byte_1 == byte_2 { // 编译通过
// 做点什么
}
}
现在让我们回顾一下,我们前一版的程序显然是错误的,但为什么 Rust 仍然允许它通过编译呢?
答案很简单:这么做是内存安全的。
Rust 的借用检查器对程序的生命周期标记只要求到能够以静态的方式验证程序的内存安全。
Rust 会爽快地编译一个程序,即使它的生命周期标记有语义上的错误,
这带来的结果就是程序会变得过于受限。
来看一个与前一个相反的例子:Rust 的生命周期省略规则恰好在这个例子上语义是正确的,
但我们却无意中用了一些多余的显式生命周期标记写了个非常受限的方法。
#[derive(Debug)]
struct NumRef<'a>(&'a i32);
impl<'a> NumRef<'a> {
// 我的结构体是在'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<'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) 装箱的 trait 对象没有生命周期
早前我们讨论了 Rust 对函数的生命周期省略规则。Rust 同样有着对于 trait 对象的生命周期省略规则,它们是:
- 如果一个 trait 对象作为一个类型参数传递到泛型中,那么它的生命约束会从它包含的类型中推断
- 如果包含的类型中有唯一的约束,那么就使用这个约束。
- 如果包含的类型中有超过一个约束,那么必须显式指定约束。
- 如果以上都不适用,那么:
- 如果 trait 是以单个生命周期约束定义的,那么就使用这个约束
- 如果所有生命周期约束都是
'static
的,那么就使用'static
作为约束 - 如果 trait 没有生命周期约束,那么它的生命周期将会从表达式中推断,如果不在表达式中,那么就是
'static
的
这么多东西听起来超级复杂,但我们可以简单地总结为 "trait 对象的生命周期约束是从上下文中推断出来的。"
在我们看过几个例子后,我们会发现生命周期约束推断其实是很符合直觉的,我们不需要去记这些很正式的规则。
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,而引用显然有生命周期约束。
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 对象的函数重构成使用泛型的函数(或者反过来)的时候感到困惑。
我们来看看这个例子:
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();
}
这会抛出下面的编译错误:
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 || {
| ^^^^^^^^^^^^^^^^^^
很好,编译器告诉了我们怎么解决这个问题,我们来试试。
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();
}
编译通过,但这两个函数放在一块儿看起来有点怪,为什么第二个函数对 T
有 'static
约束,而第一个没有?
这个问题很刁钻。根据生命周期省略规则,Rust 自动为第一个函数推断出 'static
约束,所以两个函数实际上都有 'static
约束。
在 Rust 编译器的眼中是这样的:
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 objects 的生命周期省略规则总是对的
- Rust 编译器比我更懂我代码的语义
这个误解是前两个误解的合二为一的例子:
use std::fmt::Display;
fn box_displayable<T: Display>(t: T) -> Box<dyn Display> {
Box::new(t)
}
抛出如下错误:
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 object 推断了一个'static
的生命周期约束。
use std::fmt::Display;
fn box_displayable<T: Display + 'static>(t: T) -> Box<dyn Display> {
Box::new(t)
}
现在编译通过了,但这真的是我们想要的吗?可能是,也可能不是,编译去并没有告诉我们其它解决方法
但这个也许合适。
use std::fmt::Display;
fn box_displayable<'a, T: Display + 'a>(t: T) -> Box<dyn Display + 'a> {
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<'a>(a: &'a str, b: &'a str) -> &'a str {
| ^^^^ ^^^^^^^ ^^^^^^^ ^^^
这个错误信息建议我们给输入和输出打上相同的生命周期标记。
这么做虽然能使得编译通过,但却过度限制了返回类型。
我们真正想要的是这个:
fn return_first<'a>(a: &'a str, b: &str) -> &'a str {
a
}
要点
- Rust 对 trait object 的生命周期省略规则并不是在所有情况下都正确。
- Rust 不见得比你更懂你代码的语义。
- Rust 编译错误信息给出的修改建议可能能让你的代码编译通过,但这不一定是最符合你的要求的。
8) 生命周期可以在运行时变长缩短
误解推论
- 容器类型可以通过更换引用在运行时更改自己的生命周期
- Rust 的借用检查会进行深入的控制流分析
这过不了编译:
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`在这里析构
}
// 编译错误,`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: &'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<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 的反模式(anti-pattern)。
对这种反模式保持警惕很重要,这可以让你在看到下面这样的代码的时候更容易发现它:
// 将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 仍然会自动隐式重新借用,
所以很容易无意中遇到这样的问题:
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,如果不存在则创建并插入一个新的
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<PlayerID, Player>) {
// 因为我们不能把它们放在一起用,所以这里把返回的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>(x: &'a i32) -> &'a i32 {
x
}
fn main() {
// 输入和输出有它们自己独有的生命周期
let closure = for<'a, 'b> |x: &'a i32| -> &'b i32 { x };
// 注意:上面这行代码不是合法的语法,但可以表达出我们的意思
}
出现这种差异并没有一个好的理由。闭包最早的实现用的类型推断语义和函数不同,
现在变得没法改了,因为将它们统一起来会造成一个不兼容的改动。
那么我们要怎么样显式标注闭包的类型呢?我们可选的办法有:
fn main() {
// 转成trait object,变成不定长类型,编译错误
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 约束的时候会遵循一般函数的生命周期省略规则。
这里没有什么真正的教训和洞察,只是它就是这样的而已。
要点
- 每一门语言都有自己的小陷阱 🤷
11) 'static
引用总能强制转换为 'a
引用
我前面给出了这个例子:
fn get_str<'a>() -> &'a str; // 泛型版本
fn get_str() -> &'static str; // 'static版本
有的读者问我这两个在实践中是否有区别。一开始我也不太确定,但不幸的是,
在经过一段时间的研究之后我发现它们在实践中确实存在着区别。
所以一般来说,在操作值得时候我们可以使用 'static
引用来替换 'a
引用,
因为 Rust 会自动将 'static
引用强制转换到 'a
引用。
直觉上来讲,这没毛病,在一个要求较短生命周期引用的地方使用一个有着更长的生命周期的引用不会造成内存安全问题。
下面的代码和我们想的一样编译通过:
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()); // 编译通过
let str_ref = a_or_b(some_str, static_str_fn()); // 编译通过
}
然而,这种强制转换并不会在引用作为函数的类型签名的一部分的时候出现,所以下面这段代码无法通过编译:
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); // 编译通过
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<T> Fn() -> &'static T
到 for<'a, T> Fn() -> &'a T
这种更复杂的情况。
前者是值之间的强制转换,后者是类型之间的强制转换。
要点
- 签名为
for<'a, T> Fn() -> &'a T
的函数要比签名为for<T> 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 的借用检查总会为每个变量选择一个最短可能的生命周期,并且假定每条代码路径都会被执行
- 尽量避免将可变引用重新借用为不可变引用,不然你会遇到不少麻烦
- 重新借用一个可变引用不会终止它的生命周期,即使这个可变引用已经析构
- 每个语言都有自己的小陷阱 🤷
讨论
在这些地方讨论这篇文章
关注
在 Twitter 上关注 pretzelhammer 来获取最新的博客的更新!