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
引用
总结
讨论
关注
介绍
我曾经有过的所有这些对生命周期的误解,现在有很多初学者也深陷于此。我用到的术语可能不是标准的,所以下面列了一个表格来解释它们的用意。
短语 | 意为 |
---|---|
T |
1) 包含了所有可能类型的集合 或 2) 这个集合中的类型 |
所有权类型 | 不含引用的类型, 例如 i32 , String , Vec , 等 |
1) 借用类型 或 2) 引用类型 |
不考虑可变性的引用类型, 例如 &i32 , &mut i32 , 等 |
1) 可变引用 或 2) 独占引用 |
独占的可变引用, 即 &mut T |
1) 不可变引用 or 2) 共享引用 |
共享的不可变引用, 即 &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() {
// 所有字符串都是随机生成的