Rust-死灵书(一)

一、初识安全与非安全代码

当使用某种安全编程语言的过程中遇到了处理底层实现的需求时,程序员通常有三种选择:

  • 修改代码让编译器或者运行时环境做相关优化
  • 采取某些古怪、繁琐的奇技淫巧以实现功能需求
  • 使用另一种可以处理底层细节的语言重写代码

对于最后一个选项,程序员通常会选择C语言。某些系统也只对外暴漏了C的接口。

然而,C在使用中往往过于不安全(虽然有时是出于合理的原因)。尤其是在与其他语言交互的过程中,这种不安全性还会被放大。C和与其交互的语言必须时刻小心地确认对方的行为,以防踩到舞伴的脚趾头。

那么这和Rust有什么关系呢?

嗯……不同于C,Rust是一种安全的编程语言。

但是,和C相同的是,Rust是一种非安全的编程语言。

更准确地说,Rust是一种同时包含安全和非安全特性的编程语言。

非安全Rust和安全Rust的语法规则完全相同,只不过它允许你做一些另外的不安全的行为。

1.1 安全与非安全代码的交互方式

安全与非安全代码是靠unsafe关键字分离的,它扮演着两种语言之间接口的角色。也可以在代码根部添加#![forbid(unsafe_code)]以保证你只会写安全的代码。

unsafe关键字有两层含义:声明代码中存在编译器无法检查的安全规范,同时声明开发者会自觉遵守相关规范而不会主动破坏它。

unsafe可以给函数、trait、代码块添加。

标准库也有一些非安全函数,包括:

  • slice::get_unchecked,可接受不受检查的索引值,也就是存在内存安全机制被破坏的可能
  • mem::transmute,将值重新解析成另一种类型,即允许随意绕过类型安全机制的限制(详情参考类型转换)
  • 所有指向确定大小类型(sized type)的裸指针都有offset方法,当传入的偏移量越界时将导致未定义行为(Undefined Behavior)。
  • 所有FFI(Foreign Function Interface)函数都是unsafe的,因为其他的语言可以做各种的操作而Rust编译器无法检查它。

另外有两个需要注意的非安全的trait:

  • Send是一个标志trait(即没有任何方法的trait),承诺所有的实现都可以安全地发送(move)到另一个线程。
  • Sync也是一个标志trait,承诺线程可以通过共享的引用共享它的实现。

一个trait是否应该标志为unsafe是API设计上的选择。Rust通常会尽量避免这么做,因为它会导致非安全Rust的滥用,这并不是设计者们希望看到的。

SendSync是会被各种类型自动实现的,只要这种实现可以被证明是安全的。如果一种类型其所有的值的类型都实现了Send,它本身就会自动实现Send;如果一种类型其所有的值的类型都实现了Sync,它本身就会自动实现Sync。将它们设为unsafe实际减少了非安全代码的滥用。

1.2 非安全Rust能做什么

非安全Rust比安全Rust可以多做的事情只有以下几个:

  • 解引用裸指针
  • 调用非安全函数(包括C语言函数,编译器内联函数,还有直接内存分配等)
  • 实现非安全trait
  • 访问或修改可变静态变量

这些操作被归为非安全的,是因为使用得不正确就会导致可怕的未定义行为。一旦触发了未定义行为,编译器就可以放飞自我,肆意破坏你的程序。

Rust充分限制了可能出现的未定义行为的种类。语言核心只需要防止这几种行为:

  • 解引用null指针,悬垂指针,或者未赋值的指针
  • 读取未初始化的内存
  • 破坏指针混淆规则
  • 创建非法的基本类型:
    • 悬垂引用与null引用
    • 空的fn指针
    • 0和1以外的bool类型值
    • 未定义的枚举类型的项
    • 在[0x0,0xD&FF]和[0xE000, 0x10FFFF]以外的char类型值
    • 非utf-8编码的str
  • 不谨慎地调用其他语言
  • 数据竞争

Rust语言自身可以导致未定义行为的操作就只有这些。当然,非安全函数和trait可以声明自己专有的安全规范,要求开发者必须遵守以避免未定义行为。

违背这些专有的规范通常也只是间接地触发上面列出的行为。另外,编译器内联函数也可能引入一些规则,一般是针对代码优化的假设条件。比如,Vec和Box使用的内联函数要求传入的指针永远不能为null。

Rust对于一些模糊的操作则通常比较宽容。Rust会认为下列操作是安全的:

  • 死锁
  • 竞争条件
  • 内存泄漏
  • 调用析构函数失败
  • 整型值溢出
  • 终止程序
  • 删除产品数据库

当然,有以上行为的程序极有可能就是错误的。Rust提供了一系列的工具减少这种事情的发生,但是完全地杜绝它们其实是不现实的。

1.3 编写非安全代码

Rust通常要求我们明确限制非安全Rust代码的作用域。可是,现实情况其实要更复杂一些。举个例子,看一下下面的代码:

fn index(idx: usize, arr: &[u8]) -> Option<u8> {
if idx < arr.len() {
unsafe {
Some(*arr.get_unchecked(idx))
}
} else {
None
}
}

这个函数是安全和正确的。我们检查了索引值有没有越界。如果没有,就从数组中用不安全的方式取出对应的值。然而,哪怕是这么简单的一个函数,unsafe代码块的范围也不是绝对明确的。想象一下,如果把 <改成 <=
fn index(idx: usize, arr: &[u8]) -> Option<u8> {
if idx <= arr.len() {
unsafe {
Some(*arr.get_unchecked(idx))
}
} else {
None
}
}

这段程序就有潜在的问题了,但我们其实只修改了安全代码的部分。这是安全机制的一个根本性问题:非本地性。意思是,非安全代码的稳定性其实依赖于另一些“安全”代码的状态。

是否进入非安全代码块,并不受其他部分代码正确性的影响,从这个角度看安全机制是模块化的。比如,是否对一个slice进行不安全索引,不受slice是不是null或者是不是包含未初始化的内存这些事情的影响。但是,由于程序本身是有状态的,非安全操作的结果实际依赖于其他部分的状态,从这个角度看安全机制又是非模块化的。

处理持久化状态时,非本地性带来的问题就更加明显了。看一下Vec的一个简单实现:

use std::ptr;
// 注意:这个定义十分简单。参考实现Vec的章节
pub struct Vec<T> {
ptr: *mut T,
len: usize,
cap: usize,
}
// 注意:这个实现未考虑大小为0的类型。参考实现Vec的章节
impl<T> Vec<T> {
pub fn push(&mut self, elem: T) {
if self.len == self.cap {
// 与例子本身无关
self.reallocate();
}
unsafe {
ptr::write(self.ptr.offset(self.len as isize), elem);
self.len += 1;
}
}
}

这段代码很简单,便于审查和修改。现在考虑给它添加一个新的方法:
fn make_room(&mut self) {
// 增加容量
self.cap += 1;
}

这段代码是100%的安全Rust但是彻底的不稳定。改变容量违反了Vec的不变性(cap表示分配给Vec的空间大小)。Vec的其他部分并不会保护它,我们只能信任它的值是正确的,因为本来没有修改它的方法。

因为代码逻辑依赖于struct的某个成员的不变性,那段unsafe的代码不仅仅污染了它所在的函数,它还污染了整个module。一般来说,只有在一个私有的module里非安全代码才可能是真正安全的。

这允许我们基于一些复杂的不变性写一些绝对安全的抽象。在考虑安全Rust和非安全Rust的关系时,这一点非常重要。

二、数据布局

2.1 repr(Rust)

首先,每种类型都有一个数据对齐属性(alignment)一种类型的对齐属性决定了哪些内存地址可以合法地存储该类型的值。如果对齐属性是n,那么它的值的存储地址必须是n的倍数。所以,对齐属性2表示值只能存储在偶数地址里,1表示值可以存储在任何的地方。对齐属性最小为1,并且永远是2的整数次幂。虽然不同平台的行为可能会不同,但大部分情况下基础类型都是按照它的类型大小对齐的。特别的是,在x86平台上u64f64都是按照32位对齐的。

一种类型的大小都是它对齐属性的整数倍,这保证了这种类型的值在数组中的偏移量都是其类型尺寸的整数倍,可以按照偏移量进行索引。需要注意的是,动态尺寸类型的大小和对齐可能无法静态获取。

Rust有如下几种复合类型:

  • 结构体(带命名的复合类型 named product types)
  • 元组(匿名的复合类型 anonymous product types)
  • 数组(同类型数据集合 homogeneous product types)
  • 枚举(带命名的标签联合体 named sum types — tagged unions)

如果枚举类型的变量没有关联数据,它就被称之为无成员枚举。

结构体的对齐属性等于它所有成员的对齐属性中最大的那个。Rust会在必要的位置填充空白数据,以保证每一个成员都正确地对齐,同时整个类型的尺寸是对齐属性的整数倍。例如:

struct A {
a: u8,
b: u32,
c:u16,
}

在对齐属性与类型尺寸相同的平台上,这个结构体会按照32位对齐。整个结构体的类型尺寸是32位的整数倍。它实际会转变成这样:
struct A {
a: u8,
_pad1: [u8; 3], // 为了对齐b
b: u32,
c: u16,
_pad2: [u8; 2], // 保证整体类型尺寸是4的倍数
// (译注:原文就是“4的倍数”,但似乎“32的倍数”才对)
}

这里所有的类型都是直接存储在结构体中的,成员类型和结构体之间没有其他的中介。这一点和C是一样的。但是除了数组以外(数组的子类型总是按顺序紧密排列),其他的复合类型的数据分布规则并不一定是固定不变的。对于下面两个结构体定义:
struct A {
a: i32,
b: u64,
}
struct B {
a: i32,
b: u64,
}

Rust可以保证A的两个实例的数据布局是完全相同的。但是Rust目前不保证A的实例和B的实例有着一样的数据填充和成员顺序,虽然看起来他们似乎就应该是一样的才对。

对于上面的A和B来说,这一点大概显得莫名其妙。可是当Rust要处理更复杂的数据布局问题时,它就变得很有必要了。

例如,对于这个结构体:

struct Foo<T, U> {
count: u16,
data1: T,
data2: U,
}

现在考虑范型Foo<u32, u16>Foo<u16, u32>。如果Rust按照代码中指定的顺序布局结构体成员,那么它就必须填充数据以符合对齐规则。所以,如果Rust不改变成员顺序的话,他们实际上会变成这样:
struct Foo<u16, u32> {
count: u16,
data1: u16,
data2: u32,
}
struct Foo<u32, u16> {
count: u16,
_pad1: u16,
data1: u32,
data2: u16,
_pad2: u16,
}

后者显然太浪费内存了。所以,内存优化原则要求不同的范型可以有不同的成员顺序。

枚举把这件事搞得更复杂了。举一个简单的枚举类型为例:

enum Foo {
A(u32),
B(u64),
C(u8),
}

它的布局会是这样:
struct FooRepr {
data: u64, // 根据tag的不同,这一项可以为u64,u32,或者u8
tag: u8, // 0 = A, 1 = B, 2 = C
}

这也确实就是一般情况下枚举的布局方式。

但是,在很多情况下这种表达方式并不是效率最高的。一个典型场景就是Rust的“null指针优化”:如果一个枚举类型只包含一个单值变量(比如None)和一个(级联的)非null指针变量(比如&T),那么tag其实是不需要的,因为那个单值变量完全可以用null指针来表示。所以,size_of::<Option<&T>>() == size_of::<&T>(),这个比较的结果是正确的。

Rust中的许多类型都包含或者本身就是非null指针,比如Box<T>Vec<T>String&T以及&mut T。同样的,你或许也能想到,对于级联的枚举类型,Rust会把多个tag变量合并为一个,因为它们本来就只有几个有限的可能取值。大体说来,枚举类型会运用复杂的算法确定各种级联类型的二进制表达方法。因为这件事很重要,我们把枚举的问题留到后面讨论。

2.2 类型中的奇行种

大部分情况下,我们考虑的都是拥有固定的正数尺寸的类型。但是,并非所有类型都是这样。

2.2.1 动态尺寸类型(DST, Dynamically Sized Type)

Rust支持动态尺寸类型,即不能静态获取尺寸或对齐属性的类型。乍一看,这事有点荒谬——Rust必须知道一种类型的大小和对齐方式才能正确地使用它啊!从这一点来看,DST不是一个普通的类型。由于类型大小是未知的,只能通过某种指针来访问它。所以,一个指向DST的指针是一个“胖”指针,它包含指针本身和一些额外的信息(具体请往下看)。

语言提供了两种主要的DST:trait对象和slice。

trait对象表示实现了某种指定trait的类型。具体的类型被擦除了,取而代之的是运行期的一个虚函数表,表中包含了使用这种类型所有必要的信息。这就是trai对象的额外信息:一个指向虚函数表的指针。

slice简单来说是一个连续存储结构的视图——最典型的连续存储结构是数组或Vec。slice对应的额外信息就是它所指向元素的数量。

结构体可以在最后的位置上保存一个DST,但是这样结构体本身也就变成了一个DST。

// 不能直接存储在栈上
struct Foo {
info: u32,
data: [u8],
}

2.2.2 零尺寸类型(ZST, Zero Sized Type)

Rust实际允许一种类型不占用内存空间:

struct Foo; // 没有成员 = 没有尺寸
// 所有成员都没有尺寸 = 没有尺寸
struct Baz {
foo: Foo,
qux: (), // 空元组没有尺寸
baz: [u8; 0], // 空数组没有尺寸
}

对于其自身来说,ZST显然没有任何用处。但是,和Rust中许多奇怪的布局选项一样,它的作用只在特定的上下文中才能体现:Rust认为所有产生或存储ZST的操作都可以被视为无操作(no-op)。首先,存储它没有什么意义——它又不占用空间。而且这种类型实际上只有一个值,所以加载它的操作可以凭空变一个值出来——而这种操作依然是no-op,因为产生的值不占用空间。

ZST的一个最极端的例子是Set和Map。已经有了类型Map<Key, Value>,那么要实现Set<Key, Value>的通常做法是简单封装一个Map<Key, UselessJunk>。很多语言不得不给UselessJunk分配空间,还要存储、加载它,然后再什么都不做直接丢弃它。编译器很难判断出这些行为实际是不必要的。

但是在Rust里,我们可以直接认为Set<Key> = Map<Key, ()>。Rust静态地知道所有加载和存储操作都毫无用处,也不会真的分配空间。结果就是,这段范型代码直接就是HashSet的一种实现,不需要HashMap对值做什么多余的处理。

安全代码不用关注ZST,但是非安全代码必须考虑零尺寸类型带来的影响。特别注意,计算指针的偏移量是no-op,标准的内存分配器(Rust默认使用jemalloc)在需要分配空间大小为0时可能返回nullptr,很难区分究竟是这种情况还是内存不足。

2.2.3 空类型

Rust甚至也支持不能被实例化的类型。这种类型只有类型,而没有对应的值。空类型可以通过指定没有变量的枚举来声明它:

enum Void {} // 没有变量 = 空类型

空类型比ZST更加少见。一个主要的应用场景是在类型层面声明不可到达性(unreachability)。比如,假设一个API一般需要返回一个Result,但是在某个特殊场景下它是绝对不会出错的。这种情况在类型层面的处理方法是将返回值设为Result<T, Void>。因为不可能产生一个Void类型的值,所以返回值不可能是一个Err。知道了这一点,API的调用者就可以信心十足地使用unwrap

原则上来说,Rust可以基于这一点做一些很有意思的分析和优化。比如,Result<T, Void>可以表示成 T,因为实际上不存在返回Err的情况。下面的代码曾经也可以成功编译:

enum Void {}
let res: Result<u32, Void> = Ok(0);
// 不存在Err的情况,所以Ok实际上永远都能匹配成功
let Ok(num) = res;

但是现在这些把戏已经不让玩了。所以Void唯一的用处就是明确地告诉你某些情况永远不会发生。

关于空类型的最后一个坑,创建指向空类型的裸指针实际上是合法的,但是对它解引用是一个未定义行为,因为这么做没有任何意义。也就是说,你可以使用*const Void模拟C语言的void *类型,但是使用*const ()却不会得到任何东西,因为这个函数对于随机解引用是安全的。

2.3 其他repr

2.3.1 repr(C)

这是最重要的一种repr。它的目的很简单,就是和C保持一致。数据的顺序、大小、对齐方式都和你在 C 或 C++ 中见到的一摸一样。所有你需要通过 FFI 交互的类型都应该有repr(C),因为C是程序设计领域的世界语。而且如果我们要在数据布局方面玩一些花活的话,比如把数据重新解析成另一种类型,repr(C)也是很有必要的。

但是,一定不要忘了Rust的那几个奇行种。repr(C)的存在有双重作用,既为了FFI同时也为了常规的布局控制,所以它可以被应用于那些在FFI中没有意义甚至会产生错误的类型。

  • 尽管标准的C语言不支持大小为0的类型,但ZST的尺寸仍然是0。而且它也与C++中的空类型有着明显的不同,C++的空类型还是要占用一个字节的空间的。
  • DST的指针(胖指针),元组,和带有成员变量的枚举都是C中没有的,因此也不是FFI安全的。
  • 如果T是一个FFI安全的非空指针,那么Option<T>可以保证和T拥有相同的布局和ABI,当然它也会是FFI安全的。这一规则适用于&, &mut函数指针等所有非空的指针。
  • repr(C)中元组结构体与结构体基本相同,唯一的不同是其成员都是未命名的。
  • 对于枚举的处理和repr(u*)是相同的。选择的类型尺寸等于目标平台上C的应用二进制接口(ABI)的默认枚举尺寸。注意C中枚举的数据布局是确定的,所以这确实是一种“最合理的假设”。不过,当目标C代码编译时加了一些特殊的编译器参数时,这一点可能就不正确了。
  • repr(C)repr(u*)中无成员的枚举不能被赋值为一个没有对应变量的整数,尽管在C\C++中这是一种合法的行为。构建一个没有对应变量的枚举类型实例属于未定义行为。(对于存在准确匹配的值是允许正常编写和编译的)
2.3.2 repr(u), repr(i)

这两个可以指定无成员枚举的大小。如果枚举变量对应的整数值对于设定的大小越界了,将产生一个编译期错误。你可以手工设置越界的元素为0以避免编译错误,不过要注意Rust是不允许一个枚举中的两个变量拥有相同的值的。

“无成员枚举”的意思是枚举的每一个变量里都不关联数据。不指定repr(u*)repr(i*)的无成员枚举依然是一个Rust的合法原生类型,它们都没有固定的ABI表示方法。给它们指定repr使其有了固定的类型大小,方便在ABI中使用。

Rust中所有有成员的枚举都没有确定的ABI表示方式(即使关联的数据只是PhantomData或者零尺寸类型的数据)。

为枚举显式指定repr后空指针优化将不再起作用。

这些repr对于结构体无效。

2.3.3 repr(packed)

repr(packed)强制Rust不填充空数据,各个类型的数据紧密排列。这样有助于提升内存的使用效率,但很可能会导致其他的副作用。

尤其是大部分平台都强烈建议数据对齐。这意味着加载未对齐的数据会很低效(x86),甚至是错误的(一些ARM芯片)。像直接加载或存储打包的(packed)成员变量这种简单的场景,编译器可能可以用shift和mask等方式隐藏对齐问题。但是如果是使用一个打包的变量的引用,编译器很可能没办法避免未对齐加载问题。

repr(packed)不应该随便使用。只有在你有一些极端的需求的情况下才该用它。

这个reprrepr(C)repr(Rust)的修饰器。

三、所有权

它让Rust可以彻底告别垃圾回收,同时做到内存安全和高效率。在涉及到所有权系统的细节之前,我们先看一下这种设计的目的。

看一下这个曾纠缠过每一个使用过非GC语言的人的简单错误:

fn as_str(data: &u32) -> &str {
// 计算字符串
let s = format!("{}", data);
// 哎呀!我们返回了一个只在函数内部存在的东西的引用
// 悬垂指针!释放后引用!指针别名!
// (当然这段代码在Rust中不能编译)
&s
}

这正是Rust的所有权系统要解决的问题。Rust知道&s生效的作用域,所以可以避免出现逃逸。

3.1 引用

有两种引用的类型:

  • 共享指针:&
  • 可变指针:&mut

它们遵守以下的规则:

  • 引用的生命周期不能超过被引用内容
  • 可变引用不能存在别名(alias)

就这些。这就是全部的引用模型。

当然,我们可能需要定义一下别名(alias)是什么意思。

error[E0425]: cannot find value `aliased` in this scope
--> <rust.rs>:2:20
|
2 | println!("{}", aliased);
| ^^^^^^^ not found in this scope
error: aborting due to previous error

很不幸,Rust实际上没有定义别名模型。

在Rust的开发者从语义层面确定别名的意义之前,我们先在下一章讨论一般意义上的别名指什么,还有它为什么很重要。

3.2 别名

首先,有几点重要声明:

  • 以下的讨论将采用最广泛意义上的别名的定义。而Rust的定义可能会更加严格,需要考虑到可变性和生命周期。
  • 我们假设程序都是单线程且不会中断的,同时也不会去考虑存储器映射之类的问题。除非特别指定,否则Rust默认这些事情不存在。

基于这些,我们给出定义:当变量和指针表示的内存区域有重叠时,它们互为对方的别名。

3.2.1 为什么别名很重要

看下面这个简单的函数。

fn compute(input: &u32, output: &mut u32) {
if *input > 10 {
*output = 1;
}
if *input > 5 {
*output *= 2;
}
}

我们可能会这样优化它:
fn compute(input: &u32, output: &mut u32) {
let cached_input = *input; // 将*input放入缓存
if cached_input > 10 {
*output = 2; // x > 10 则必然 x > 5,所以直接加倍并立即退出
} else if cached_input > 5 {
*output *= 2;
}
}

在Rust中,这种优化是正确的。但对于其他几乎所有的语言,都是有错误的(除非编译器进行全局分析)。这是因为优化方案成立的前提是不存在别名,而绝大多数语言并不会限制这一点。例子中我们需要特别担心的是传递给inputoutput的参数可能会重合,比如comput(&x, &mut x)

对于上面的参数,程序流程会是这样:

                  //  input ==  output == 0xabad1dea
// *input == *output == 20
if *input > 10 { // true (*input == 20)
*output = 1; // 同时覆盖了 *input,以为他们是一样的
}
*input > 5 { // false (*input == 1)
*output *= 2;
}
// *input == *output == 1

我们优化过的函数的结果是*output == 2,所以对于这样的输入参数,优化函数是不正确的。

在Rust中我们知道不会出现上面那样的输入参数,因为&mut不允许存在别名。所以我们可以安全的忽略这种可能性而使用优化方案。对于大多数其他语言,这种输入的可能性是存在的,必须特别的考虑到。

这就是别名分析的重要性:它允许编译器做出一些有用的优化。举几个例子:

  • 将值放入缓存变量中,因为可以确定没有指针可以访问变量的内存。
  • 省略一些读操作,因为可以确定在上一次读内存之后,内存没有发生变化
  • 省略一些写操作,因为可以确定下一次写内存之前,内存不会被读取
  • 移动或重排读写操作的顺序,因为可以确定它们并不互相依赖

这些优化也可以进一步证明更大程度的优化的可行性,比如循环向量化、常量替换和不可达代码消除等。

在前面的例子中,我们根据&mut u32不存在别名的原则证明了*output不可能影响*input。这使得我们缓存了*input,并且省略了一次读操作。

通过缓存读操作的结果,我们知道在>10的分支中的写操作不会影响执行>5分支的判断条件,这样我们在*input > 10的情况下省略了一次读-改-写操作(*output加倍)。

关于别名分析需要记住的一个关键点是,写操作是优化的主要障碍。我们不能随意移动读操作的唯一原因,就是可能存在向相同位置写数据的操作,这种移动会破坏他们之间的顺序关系。

比如,下面这个版本的函数中,我们不需要担心别名问题,因为我们把唯一的一次写*output的操作放到了函数的最后。这让我们可以随意地改变之前的读*input操作的顺序:

fn compute(input: &u32, output: &mut u32) {
let mut temp = *output;
if *input > 10 {
temp = 1;
}
if *input > 5 {
temp *= 2;
}
*output = temp;
}

我们仍然需要别名分析来证明temp不是input的别名,但是这时的证明过程要简单得多:一个本地别量不可能是在它的声明之前就存在的变量的别名。这是所有编程语言共有的一个前提,所以这一版本的函数可以按照与其他语言相同的方式去优化它。

这也就是Rust可能采用的“别名”定义与生命周期和可变性有关的原因:在没有写内存操作存在的情况下,我们实际上不需要关注是否存在别名。

当然,一个完整的别名模型也要考虑到诸如函数调用(可能改变我们不可见的内容)、裸指针(不存在限制别名的需求),以及UnsafeCell(允许被&引用的内容可变)。

3.3 生命周期

Rust在整个生命周期里强制执行生命周期的规则。生命周期说白了就是作用域的名字。每一个引用以及包含引用的数据结构,都要有一个生命周期来指定它保持有效的作用域。

在函数体内,Rust通常不需要你显式地给生命周期起名字。这是因为在本地上下文里,一般没有必要关注生命周期。Rust知道程序的全部信息,从而可以完美地执行各种操作。它可能会引入许多匿名或者临时的作用域让程序顺利执行。

但是如果你要跨出函数的边界,就需要关心生命周期了。生命周期用这样的符号表示:'a,'static。为了更清晰地了解生命周期,我们假设我们可以为生命周期打标签,去掉本章所有例子的语法糖。

最开始,我们的示例代码对作用域和生命周期使用了很激进的语法糖特性——甜得像玉米糖浆一样,因为把所有的东西都显式地写出来实在很讨厌。所有的Rust代码都采用比较激进的理论以省略“显而易见”的东西。

一个特别有意思的语法糖是,每一个let表达式都隐式引入了一个作用域。大多数情况下,这一点并不重要。但是当变量之间互相引用的时候,这就很重要了。举个简单的例子,我们彻底去掉下面这段代码的语法糖:

let x = 0;
let y = &x;
let z= &y;

借用检查器通常会尽可能减少生命周期的范围,所以去掉语法糖后的代码大概像这样:
// 注意:'a: { 和 &'b x 不是合法的语法
'a: {
let x: i32 = 0;
'b: {
// 生命周期是'b,因为这就足够了
let y: &'b i32 = &'b x;
'c: {
// 'c也一样
let z: &'c &'b i32 = &'c y;
}
}
}

这样的写法……太可怕了。我们先停下来感谢Rust把这一切都简化掉了。

将引用传递到作用域以外会导致生命周期扩大:

let x = 0;
let z;
let y = &x;
z = y;

'a: {
let x: i32 = 0;
'b: {
let z: &'b i32;
'c: {
// 必须使用'b,因为引用被传递到了'b的作用域
let y: &'b i32 = &'b x;
z = y;
}
}
}

3.3.1 示例:引用超出被引用内容生命周期

好了,让我们再看一遍曾经举过的一个例子:

fn as_str(data: &u32) -> &str {
let s = format!("{}", data);
&s
}

去掉语法糖:
fn as_str<'a>(data: &'a u32) -> &'a str {
'b: {
let s = format!("{}", data);
return &'a s;
}
}

函数as_str的签名里接受了一个带有生命周期的u32类型的引用,并且保证会返回一个生命周期一样长的str类型的引用。从这个签名我们就已经可以看出问题了。它表示我们必须到那个u32引用的作用域,或者比它还要早的作用域里去找一个str。这就有点不合理了。

接下来我们生成一个字符串s,然后返回它的引用。我们的函数要求这个引用的有效期不能小于'a,那是我们给引用指定的生命周期。不幸的是,s是在作用域’b里面定义的除非’b包含’a这个函数才可能是正确的——而这显然不可能,因为’a必须包含它所调用的函数。这样我们创建了一个生命周期超出被引用内容的引用,这明显违背了之前提到的引用的第一条规则。编译器十分感动然后拒绝了我们。

我们扩展一下这个例子,以便看得更清楚:

fn as_str<'a>(data: &'a u32) -> &'a str {
'b: {
let s = format!("{}", data);
return &'a s;
}
}
fn main() {
'c: {
let x: u32 = 0;
'd: {
// 这里引入了一个匿名作用域,因为借用不需要在整个x的作用域内生效
// as_str的返回值必须引用一个在函数调用前就存在的str
// 显然事实不是这样的。
println!("{}", as_str::<'d>(&'d x));
}
}
}

完蛋了!

当然,这个函数的正确写法应该是这样的。

fn to_string(data: &u32) -> String {
format!("{}", data)
}

我们必须创建一个值然后连同它的所有权一起返回。除非一个字符串是&'a u32的成员,我们才能返回&'a str,显然事情并不是这样的。

(其实我们也可以返回一个字符串的字面量,它是一个全局的变量,可以认为是处于栈的底部。尽管这样极大限制了函数的使用场合。)

3.3.2 示例:存在可变引用的别名

在看另一个老的例子:

let mut data = vec![1, 2,3];
let x = &data[0];
data.push(4);
println!("{}", x);

'a: {
let mut data: Vec<i32> = vec![1, 2, 3];
'b: {
// 对于这个借用来说,'b已经足够大了
// (借用只需要在println!中生效即可)
let x: &'b i32 = Index::index::<'b>(&'b data, 0);
'c: {
// 引入一个临时作用域,因为&mut不需要存在更长时间
Vec::push(&'c mut data, e);
}
println!("{}", x);
}
}

这里的问题更加微妙也更有趣。我们希望Rust出于如下的原因拒绝编译这段代码:我们有一个有效的指向data的内部数据的引用x,而同时又创建了一个data的可变引用用于执行push。也就是说出现了可变引用的别名,这违背了引用的第二条规则。

但是Rust其实并非因为这个原因判断这段代码有问题。Rust不知道xdata的子内容的引用,它其实完全不知道Vec的内部是什么样子的。它只知道x必须在'b范围内有效,这样才能打印其中的内容。函数Index::index的签名因此要求传递的data的引用也必须在'b的范围内有效。当我们调用push的时候,Rust发现我们要创建一个&'c mut data。它知道'c是包含在'b以内的,因为&'b data还存活着,所以它拒绝了这段程序。

我们看到了生命周期系统要比引用的保护措施更加简单粗暴。大多数情况下这也没什么,它让我们不用没完没了地向编译器解释我们的程序。但是这也意味着许多语义上正确的程序会被编译器拒绝,因为生命周期的规则太死板了。

3.4 生命周期的局限

考虑下面的代码:

struct Foo;
impl Foo {
fn mutate_and_share(&mut self) -> &Self {&*self}
fn share(&self) {}
}
fn main() {
let mut foo = Foo;
let loan = foo.mutate_and_share();
foo.share();
}

你可能觉得它能成功编译。我们调用mutate_and_share,临时可变地借用foo,但接下来返回一个共享引用。因为调用foo.share()时没有可变的引用了,所以我们认为可以正常调用。

但是当我们尝试编译它:

<anon>:11:5: 11:8 error: cannot borrow `foo` as immutable because it is also borrowed as mutable
<anon>:11 foo.share();
^~~
<anon>:10:16: 10:19 note: previous borrow of `foo` occurs here; the mutable borrow prevents subsequent moves, borrows, or modification of `foo` until the borrow ends
<anon>:10 let loan = foo.mutate_and_share();
^~~
<anon>:12:2: 12:2 note: previous borrow ends here
<anon>:8 fn main() {
<anon>:9 let mut foo = Foo;
<anon>:10 let loan = foo.mutate_and_share();
<anon>:11 foo.share();
<anon>:12 }

发生了什么呢?嗯……我们遇到了和上一章的示例2相同的错误。我们去掉语法糖,会得到这样的代码:
struct Foo;
impl Foo {
fn mutate_and_share<'a>(&'a mut self) -> &'a Self { &'a *self }
fn share<'a>(&'a self) {}
}
fn main() {
'b: {
let mut foo: Foo = Foo;
'c: {
let loan: &'c Foo = Foo::mutate_and_share::<'c>(&'c mut foo);
'd: {
Foo::share::<'d>(&'d foo);
}
}
}
}

生命周期系统强行把&mut foo的生命周期扩展到’c,以和loan的生命周期以及mutate_and_share的签名匹配。接下来我们调用share,Rust认为我们在给&'c mut foo创建别名,于是拒绝了我们。

这段程序显然完全符合引用的语义,但是我们的生命周期系统过于粗糙,无法对它进行正确的分析。

注:这里依旧没有看明白问题的所在。

3.5 省略生命周期

为了让语言的表达方式更人性化,Rust允许函数的签名中省略生命周期。

“生命周期位置”指的是你在类型中可以写生命周期的地方。

&'a T
&'a mut T
T<'a>

生命周期的位置可以在“输入”也可以在“输出”:

  • 对于fn定义的函数,“输入”指的是函数签名中的参数的类型,而“输出”是结果的类型。所以fn foo(s: &str) -> (&str, &str)省略了一个在输入位置处的生命周期和两个结果位置的生命周期。注意,fn方法定义中的输入位置不包括impl头处的生命周期(自然地,对于trait的默认方法,也不包括trait的头的位置)。
  • 在未来,应该也可能会省略impl头位置处的生命周期。

省略的规则如下:

  • 每一个在输入位置省略的生命周期都对应一个唯一的生命周期参数。
  • 如果只有一个输入的生命周期位置(无论省略还是没省略),那个生命周期会赋给所有省略了的输出生命周期。
  • 如果有多个输入生命周期位置,而其中一个是&self或者&mut self,那么self的生命周期会赋给所有省略了的输出生命周期。
  • 除了上述两种情况,其他省略生命周期的情况都是错误的。
fn print(s: &str);                                      // 省略的
fn print<'a>(s: &'a str); // 完整的
fn debug(lvl: usize, s: &str); // 省略的
fn debug<'a>(lvl: usize, s: &'a str); // 完整的
fn substr(s: &str, until: usize) -> &str; // 省略的
fn substr<'a>(s: &'a str, until: usize) -> &'a str; // 完整的
fn get_str() -> &str; // 错误
fn frob(s: &str, t: &str) -> &str; // 错误
fn get_mut(&mut self) -> &mut T; // 省略的
fn get_mut<'a>(&'a mut self) -> &'a mut T; // 完整的
fn args<T: ToCStr>(&mut self, args: &[T]) -> &mut Command // 省略的
fn args<'a, 'b, T: ToCStr>(&'a mut self, args: &'b [T]) -> &'a mut Command // 完整的
fn new(buf: &mut [u8]) -> BufWriter; // 省略的
fn new<'a>(buf: &'a mut [u8]) -> BufWriter<'a> // 完整的

3.6 无界生命周期

非安全代码经常会凭空变出来一些引用和生命周期。这些生命周期都是无界的。最常见的场景是解引用一个裸指针,然后产生一个拥有无界生命周期的引用。这些生命周期根据上下文的要求,想要多大就可以有多大。这其实比简单的设为'static更加强大。比如&'static &'a T是无法通过类型检查的,但是无界生命周期可以完美适配&'a &'a T。不过大多数情况下,这种的无界生命周期会被视为'static

几乎没有哪个引用是'static,所以这样很可能是错误的。transmutetransmute_copy是两种很主要的例外情况。我们应该尽量早的确定无界生命周期的边界,特别是在涉及到函数调用的情况下。

对于一个函数,任何不是从输入那里来的输出生命周期都是无界的。比如:

fn get_str<'a>() -> &'a str;

这个函数会产生一个拥有无界生命周期的&str。最简单的避免无界生命周期的方式就是在函数声明中运用生命周期省略。如果一个输出生命周期被省略了,它必须受限于一个输入生命周期。当然它有可能被赋予了一个错误的生命周期,但是这样通常只会产生一个编译错误,总比允许它破坏内存安全要好。

在函数的内部,限制生命周期范围是极容易出错的。最安全且简单的限制生命周期的方法是将它作为一个有有界生命周期的函数的返回值。但是,如果这个不被接受,引用可以被设置成一个特别的生命周期。不幸的是,我们不可能为函数所有的生命周期命名。

注:这里依旧没有看明白问题的所在。

3.7 高阶trait边界(HRTB)

Rust的Fn trait是个神奇的存在。比如,我们可以写出这样的代码:

struct Closure<F> {
data: (u8, u16),
func: F
}
impl<F> Closure<F>
where F: Fn(&(u8, u16)) -> &u8,
{
fn call(&self) -> &u8 {
(self.func)(&self.data)
}
}
fn do_it(data: &(u8, u16)) -> &u8 { &data.0 }
fn main() {
let clo = Closure{ data: (0, 1), func: do_it };
println!("{}", clo.call());
}

如果我们像在生命周期那一章里一样地去掉这段代码的语法糖,我们会发现一些问题:
struct Closure<F> {
data: (u8, u16),
func: F,
}
impl<F> Closure<F>
// where F: Fn(&'??? (u8, u16)) -> &'??? u8,
{
fn call<'a>(&'a self) -> &'a u8 {
(self.func)(&self.data)
}
}
fn do_it<'b>(data: &'b (u8, u16)) -> &'b u8 { &'b data.0 }
fn main() {
'x: {
let clo = Closure { data: (0, 1), func: do_it };
println!("{}", clo.call());
}
}

我们究竟应该怎么表示F的trait边界里的生命周期呢?这里需要一个生命周期,但是在我们进入call函数之前我们都不知道生命周期的名字!而且,那里的生命周期也是不固定的,&self在那一时间点上是什么生命周期,call就也要是什么生命周期。

这里我们需要借助高阶trait边界(HRTB, Higher-Rank Trait Bounds)的神奇力量了。我们去掉语法糖之后的代码应该是这样的:

where for<'a> F: Fn(&'a (u8, u16)) -> &'a u8,

(其中Fn(a, b, c) -> d本身就是不确定的Fn trait的语法糖)

for<'a>可以读作“对于'a的所有可能选择”,基本上表示一个无限的列表,包含所有F需要满足的trait边界。不过别紧张,除了Fn trait之外我们很少会遇到需要HRTB的场景,而且即使遇到了我们还有一个神奇的语法糖相助。

3.8 子类型和变性

3.8.1 子类型

子类型是类型之间的一种关系,可以让静态类型语言更加地灵活自由。

理解这一概念最简单的方法就是参考一些支持继承特性的语言。比如说一个Animal类型有一个eat()方法,Cat类型继承了Animal并且添加了一个meow()方法。如果没有子类型机制,那么要写一个feed(Animal)函数,我们就不能给它传递Cat类型的参数,因为Cat并不是一个Animal。但是把Cat传递给需要Animal类型的地方似乎非常的合理。毕竟,Cat就是一个Animal外加一些自己的特性。这些特性完全可以被忽略,不应该妨碍我们在这里使用它!

这就是子类型机制允许我们做的事情。因为Cat是一个Animal外加一些特性,我们就可以说CatAnimal的子类型。任何需要某种类型的地方,我们都可以传递一个那种类型的子类型。很好!虽然实际情况会稍微复杂和微妙一点,但这种基本的理解足够你应对99%的应用场景了。我们在本章的后面会说明剩下的1%是什么。

尽管Rust没有结构体继承的概念,它却有子类型机制。在Rust中,子类型是针对生命周期存在的。生命周期是代码的作用域,所以我们可以根据它们相互包含的关系判断他们的继承关系。

生命周期的子类型指的是:如果'big: 'small(big包含small,或者big比small长寿),那么'big就是'small的子类型。这一点很容易弄错,因为它和我们的直觉是相反的:大的范围是小的范围的子类型。(生命周期的范围与类型泛型相反!)不过如果你对比一下我们举的Animal的例子就清楚了:Cat是一个Animal外加一些独有的东西,而'big'small外加一些独有的东西。

考虑下图:

'static  'big    
|
| | 'small_1
| | |
| |
| | 'small_2
| | |
| | |
| | |
| |
|

这张图的子类型关系应当是 'static: 'big: 'small_1'static: 'big: 'small_2。而 'static 则是所有lifetime的子类型。

换一个角度想,如果需要一个在'small内有效的引用,实际指的是至少在'small中有效的引用。我们并不在乎生命周期是不是完全的一致。从这点上来说,永久生命周期'static是所有生命周期的子类型。

高阶生命周期也是所有具体生命周期的子类型。这是因为一个随意变化的生命周期比特定的一个生命周期更通用。

(将生命周期类型化是一个过于自由的设计,以至于一些人并不赞同它。但是,把生命周期看做一种类型,这确实简化了我们的分析。)

当然你不能写一个接收'a类型的值的函数!生命周期只是别的类型的一部分,所以我们需要一些办法来处理它。这里,就要涉及到变性。

3.8.2 变性

变性显得有一点复杂。

协变和逆变都是术语,
前者指能够使用比原始指定的派生类型的派生程度更大(更具体的)的类型,
后者指能够使用比原始指定的派生类型的派生程度更小(不太具体的)的类型 。
泛型类型参数支持协变和逆变,可在分配和使用泛型类型方面提供更大的灵活性。

变性是类型构造函数与它的参数相关的一个属性。Rust中的类型构造函数是一个带有无界参数的通用类型。比如,Vec是一个构造函数,它的参数是T,返回值是vec<T>&&mut也是构造函数,它们有两个类型:一个生命周期,和一个引用指向的类型

构造函数F的变性表示了它的输入的子类型如何影响它输出的子类型。Rust中有三种变性:

  • 如果当TU的子类型时,F<T>也是F<U>的子类型,则F对于T是协变的
  • 如果当TU的子类型时,F<U>F<T>的子类型,则F对于T是逆变的
  • 其他情况(即子类型之间没有关系),则F对于T是不变的

注意,在Rust中协变性远比逆变性要普遍和重要。逆变性的存在几乎可以忽略。

一些重要的变性(下文会详细描述):

  • &'a T对于'aT是协变的
  • &'a mut T对于'a是协变的,对于T是不变的
  • fn(T) -> U对于T是逆变的,对于U是协变的
  • BoxVec以及所有的集合类对于它们保存的类型都是协变的
  • UnsafeCell<T>, Cell<T>, RefCell<T>, Mutex<T>和其他的内部可变类型对于T都是不变的
Type 在 ‘a 上的型变 在 T 上的型变
&'a T 协变的 协变的
&'a mut T 协变的 不变的
*const T 协变的
*mut T 不变的
[T] 和 [T; n] 协变的
fn() -> T 协变的
fn(T) -> () 逆变的
fn(T) -> T 不变的
std::cell::UnsafeCell<T> 不变的
std::marker::PhantomData<T> 协变的
dyn Trait<T> + 'a 协变的 不变的

我们举几个例子说明这些变性为什么是正确且必要的。

fn bar<'a>() {
let s: &'static str = "hi";
let t: &'a str = s;
}

因为 'static 比生存期参数 'a 的寿命长,所以 &'static str&'a str 的子类型。

在介绍子类型的时候,其实已经包括了为什么&'a T'a是协变的。当需要一个较短的生命周期时,我们需要能够传递一个更长的生命周期。

类似的理由也可以解释为什么它对于T是协变的:给一个要求&&'a str的地方传递&&'static T是很合理的。这种间接的引用并不影响对生命周期长度的要求。

但是同样的逻辑并不适用于&mut。下面的代码演示了为什么&mut对于T是不变的:

fn overwrite<T: Copy>(intput: &mut T, new: &mut T) {
*input = *new;
}
fn main() {
let mut forever_str: &'static str = "hello";
{
let string = String::from("world");
overwrite(&mut forever_str, &mut &*string);
}
// 不好!在打印被释放的内存数据
println!("{}", forever_str);
}

overwrite的签名显然是合法的,它接受两个相同类型的可变引用,然后用一个覆盖另外一个。

但是,如果&mut T对于T是协变的,&mut &'static str将会是&mut &'a str的子类型,这是因为&'static str&'a str的子类型。这时forever_str的生命周期就缩减到和string一样短,overwrite也可以被正常调用。接下来string被释放,等到打印的时候forever_str实际指向了一块释放后的内存空间!所以&mut必须是不变的。

这是变性的一个基本原则:如果生命周期较短的内容有可能存储在生命周期更长的变量里,这时必须要求变性是不变的。

更一般的解释是,子类型和变性可用的前提是我们可以安全地忘掉类型的细节。但对于可变引用,总有一些地方(被引用的原始值)记着类型的信息并且假设它们不会改变。如果我们改变了这些信息,原始值的位置就可能出现异常。

但是,&'a mut T对于'a却是协变的。'aT最关键的区别是'a是引用自身的属性,而T则是引用借用的。如果改变了T的类型,T的原始值依然记着它的类型。可如果改变的是生命周期的类型,只有引用自己知道这一变化,因此这是安全的。换句话说,&'a mut T拥有'a,但是仅仅借用T

BoxVex的情况就很有趣了,他们是协变的,可是你可以在里面存储值。Rust的类型系统允许它们比其他的类型更聪明。为了理解为什么拥有数据所有权的容器类型对于它们的内容是协变的,我们需要考虑两种可能发生子类型变化的方式:通过值和通过引用。

如果子类型通过值发生变化,原有的记录类型信息的位置会被移除,也意味着容器再也不能使用原有的值了。所以我们也就不用担心有其他的地方记录着类型的信息。换言之,通过值使用子类型的特性会彻底销毁原有类型的信息。例如,这段代码可以编译并正常运行:

fn get_box<'a>(str: &'a str) -> Box<&'a str> {
// 字符串字面量是&'static str类型,但是我们完全可以“忘掉”这一点,
// 就让调用者认为这个字符串的生命周期只有这么短
Box::new("hello")
}

如果子类型通过引用发生变化,那么容器类会以&mut Vec<T>类型传递。可是&mut对于它引用的值是不变的,所以&mut Vec<T>对于T实际也是不变的。那么Vec<T>对于T协变这件事在引用的情况下就完全不重要了。

不过,BoxVec的协变性在不可变引用的情况下依然有用。所以你可以将&Vec<&'static str>传递给需要&Vec<&'a str>的地方。

cell类型的不变性可以这样理解:对于cell来说&就是&mut,因为你可以通过&储存值。所以cell必须是不变的,以避免生命周期缩短的问题。

fn是最怪异的,因为它具有混合变性,而且它也是唯一用到了逆变性的地方。下面的函数签名展示了为什么fn(T) -> U对于T是逆变的:

// 'a来自父作用域
fn foo(&'a str) -> usize;

这个签名表明函数可以接受任何生命周期不小于'a&str。如果函数对于&'a str是协变的,那么这个函数
fn foo(&'static str) -> usize;

就是它的子类型并且可以使用。但是,这个函数的要求其实更严格,它只能接受&'static str,不能接受其他类型。给它传递一个&'a str是错误的,因为我们不能假设传递给它的值会永远存在。所以,函数对于它的参数类型肯定不能使协变的。

如果我们反过来应用逆变性,就万事大吉了!需要一个函数来处理永远存在的字符串,而我们提供了一个处理有限生命周期字符串的函数,这也是完全合理的。所以,

fn foo(&'a str) -> usize;

可以被传递给需要
fn foo(&'static str) -> usize;

的地方。

fn(T) -> U对于U怎么又是协变的了呢?看看下面这个函数签名:

// 'a来自父作用域
fn foo(usize) -> &'a str;

这个函数声明它将返回一个生命周期长于'a的引用。那么下面这个函数
fn foot(usize) -> &'static str;

用在这里是完全可以的,因为它的的确确返回了一个生命周期长于'a的引用。所以函数对于它的返回值是协变的。

*const&有着完全一样的语义,所以变性也是一样的。*mut正相反,它可以解引用出一个&mut,所以和cell一样,它也是不变的。

以上规则都是针对标准库提供的类型,那么自己定义的类型又如何确定变性呢?简单点说,结构体会继承它的成员的变性。如果结构体Foo有一个成员a,它使用了结构体的泛型参数A,那么Foo对于A的变性就等于a对于A的变性。可如果A被用在了多个成员中:

  • 如果所有用到A的成员都是协变的,那么Foo对于A就是协变的
  • 如果所有用到A的成员都是逆变的,那么Foo对于A也是逆变的
  • 其他的情况,Foo对于A是不变的
use std::cell::Cell;
struct Foo<'a, 'b, A: 'a, B: 'b, C, D, E, F, G, H, In, Out, Mixed> {
a: &'a A, // 对于'a和A协变
b: &'b mut B, // 对于'b协变,对于B不变
c: *const C, // 对于C协变
d: *mut D, // 对于D不变
e: E, // 对于E协变
f: Vec<F>, // 对于F协变
g: Cell<G>, // 对于G不变
h1: H, // 对于H本该是可变的,但是……
h2: Cell<H>, // 其实对H是不变的,发生变性冲突的都是不变的
i: fn(In) -> Out, // 对于In逆变,对于Out协变
k1: fn(Mixed) -> usize, // 对于Mix本该是逆变的,但是……
k2: Mixed, // 其实对Mixed是不变的,发生变性冲突的都是不变的
}

3.8 子类型和变性-more

看看下面的三个trait

trait Animal {
fn snuggle(&self);
fn eat(&mut self);
}

trait Cat: Animal {
fn meow(&self);
}

trait Dog: Animal {
fn bark(&self);
}

但是无法实现
fn love(pet: Animal) {
pet.snuggle();
}

let mr_snuggles: Cat = ...;
love(mr_snuggles); // ERROR: expected Animal, found Cat

上面简单的例子。

3.8.1 子类型解决的问题

这就是子类型希望解决的问题。因为 Cat 是 Animal+其他,所以我们描述:Cat 是 Animal 的子类型(subtype)——因为猫是所有动物中的一种;同样地,我们描述:Animal 是 Cat 的超类型(supertype)。

有了子类型,我没就可以给上述的严格静态类型系统增加一条简单的规则了:当某处希望接收类型 T 时,除 T 外还接收类型 T 的子类型。

或者更具体一点:希望接收 Animal 的地方也可以接收 Cat 或 Dog。
但是简单的应用则会发生无法确认的问题:

fn evil_feeder(pet: &mut Animal) {
let spike: Dog = ...;

// `pet` 是 Animal, Dog 是 Animal 的子类型
// 所以没问题……吧?
*pet = spike;
}

fn main() {
let mut mr_snuggles: Cat = ...;
evil_feeder(&mut mr_snuggles); // 把 mr_snuggles 替换成 Dog
mr_snuggles.meow(); // OH NO, MEOWING DOG!
}

变型(variance),一套管理子类型组成方式的规则。最重要的是,变型定义了禁止套用子类型的情形。

3.8.2 生命周期在这里的作用

这里的描述和3.8类似,'bit: 'small描述了生命周期的范围关系,'bit继承了'small。这看上去有点反直觉:大区域是小区域的子类型。但仔细想想 Animal 的例子就能明白了:Cat 是 Animal+其他,而 big 则是 small+其他。

3.8.3 变型

类型构造器 F 的变型指输入子类型如何影响输出子类型。Rust 中有三种变型。给定两个类型 Sub 和 Super,其中 Sub 是 Super 的子类型,则有:

  • 如果 F<Sub>F<Super> 的子类型,则 F 是协变(covariant)的
  • 如果 F<Super>F<Sub> 的子类型,则 F 是逆变(contravariant)的
  • 否则,F 是不变(invariant)的

接下来来看一些例子。

fn evil_feeder(pet: &mut Animal) {
let spike: Dog = ...;

// `pet` 是 Animal, Dog 是 Animal 的子类型
// 所以没问题……吧?
*pet = spike;
}

fn main() {
let mut mr_snuggles: Cat = ...;
evil_feeder(&mut mr_snuggles); // 把 mr_snuggles 替换成 Dog
mr_snuggles.meow(); // OH NO, MEOWING DOG!
}

查阅上面的变型表,发现 &mut TT不变的。也就是说,问题已经解决了:尽管 CatAnimal 的子类型,但 &mut Cat 不再是 &mut Animal 的子类型了。由此,静态类型检查器就能够阻止我们将 Cat 类型传给 evil_feeder

子类型化的合理性是基于部分细节可以忽略的前提的。&mut T 对于 T 协变的问题在于在不知道细节的前提下,我们被赋予了修改原始值的权力

3.8.4 生命周期的协变

为什么引用的生命周期是协变的呢?
生命周期引用化是 Rust 类型的根本所在。拥有类型系统的根本目的在于我们能够将长生命周期的参数传递给接收短生命周期的函数。

生命周期只是引用的一部分。引用者的类型是共享的,这就是为什么在一个地方调整这个类型会出问题的原因。但如果你在把生命周期移交给他人时削减生命周期(从 'long'short),生命周期信息就不再共享了。现在有了两个独立的、不相关的生命周期,二者也就不会互相扰乱了。

对于生命周期,我们希望将长生命周期(longest)转换为短生命周期(shortest),然后以不够长的生命周期(longer)覆盖。如下所示:

fn evil_feeder<T>(input: &mut T, val: T) {
*input = val;
}

fn main() {
let mut mr_snuggles: &'static str = "meow! :3"; // mr. snuggles forever!!
{
let spike = String::from("bark! >:V");
let spike_str: &str = &spike; // 只能活过这个 block
evil_feeder(&mut mr_snuggles, spike_str); // EVIL!
}
println!("{}", mr_snuggles); // UAF?
}

如果我们尝试运行,会得到什么结果呢?
error[E0597]: `spike` does not live long enough
--> src/main.rs:9:32
|
9 | let spike_str: &str = &spike;
| ^^^^^ borrowed value does not live long enough
10 | evil_feeder(&mut mr_snuggles, spike_str);
11 | }
| - borrowed value only lives until here
|
= note: borrowed value must be valid for the static lifetime...

让我们看看这一切到底是如何发生的。首先是新的 evil_feeder 函数:
fn evil_feeder<T>(input: &mut T, val: T) {
*input = val;
}

它接收了一个可变引用和一个值,然后用值覆盖了引用。

同时,调用者这边,我们传入了 &mut 'static str&'spike_str str

  • 由于 &mut T 对于 T 是不变的,因此编译器得出结论:第一个参数不能接收子类型。因此 T 的类型就必须是 &'static str
  • 另一个参数的类型是 &'a str,对 'a 是协变的,因此编译器约束:&'spike_str str 必须是 &'static str 的子类型,也就是说 'spike_str 必须是 'static 的子类型,也就是说 'spike_str 必须包含 'static——但只有 'static 自身能包含 'static

这就是为什么当我们试图将 &spike 赋值给 spike_str 时会出错。编译器工作的结论是 spike_str 必须永远存在,而 &spike 根本不可能活那么久。

因此,尽管在引用它们的生命周期中是协变的,但只要放到一个有问题的上下文中,它们就会继承这种不变性。在上文例子中,我们就是从 &mut T 中继承了不变性。

因此 Box(Vec、HashMap、……)之所以是协变的原因也和生命周期协变的原因相同:一旦你试图使用诸如可变引用,它们就会继承不变性,以防止坏事发生。

3.8.5 拥有所有权时的协变

Box 允许我们从值的层面关注被我们忽视的部分。和那些允许任意别名的语言不同,Rust 有着非常严格的规则:如果你能够修改或移动所有权,则你是唯一能够访问该变量的存在。

let mr_snuggles: Box<Cat> = ..;
let spike: Box<Dog> = ..;

let mut pet: Box<Animal>;
pet = mr_snuggles;
pet = spike;

这段代码没有任何问题,因为当我们移动后,我们就完全忘记了 Cat 或 Dog 存在的事实——它们只剩下了 Animal。

和不可变引用协变的原因相反,拥有所有权的值之所以协变是因为你能够改变一切。旧地与新地之间不存在任何关系,而进行子类型转换所破坏的内容也就没人知道,因此就不会因为这部分信息产生矛盾了。

3.9 Drop检查

我们已经知道生命周期给我们提供了一些很简单的规则,以保证我们永远不会读取悬垂引用。但是,到目前为止我们提到生命周期的长短时,指的都是非严格的关系。也就是说,当我们写'a: 'b的时候,'a其实也可以和'b一样长。乍一看,这一点没什么意义。本来也不会有两个东西被同时销毁的,不是吗?我们去掉下面的let表达式的语法糖看看:

let x;
let y;

{
let x;
{
let y;
}
}

每一个都创建了自己的作用域,可以很清楚地看出来一个在另一个之前被销毁。但是,如果是下面这样的呢?
let (x, y) = (vec![], vec![]);

有哪一个比另一个存活更长吗?答案是,没有,没有哪个严格地比另一个长。当然,x和y中肯定有一个比另一个先销毁,但是销毁的顺序是不确定的。并非只有元组是这样,复合结构体从Rust 1.0开始就不会保证它们的销毁顺序。

我们已经清楚了元组和结构体这种内置复合类型的行为了。那么Vec这样的类型又是什么样的呢?Vec必须通过标准库代码手动销毁它的元素。通常来说,所有实现了Drop的类型在临死前都有一次回光返照的机会。所以,对于实现了Drop的类型,编译器没有充分的理由判断它们的内容的实际销毁顺序。

可是我们为什么要关心这个?因为如果系统不够小心,就可能搞出来悬垂指针。考虑下面这个简单的程序:

struct Inspector<'a>(&'a u8);
fn main() {
let (inspector, days);
days = Box::new(1);
inspector = Inspector(&days);
}

这段程序是正确且可以正常编译的。days并不严格地比inspector存活得更长,但这没什么关系。只要inspector还存活着,days就一定也活着。

可如果我们添加一个析构函数,程序就不能编译了!

struct Inspector<'a>(&'a u8);
impl<'a> Drop for Inspector<'a> {
fn drop(&mut self) {
println!("再过{}天我就退休了!", self.0);
}
}
fn main() {
let (inspector, days);
days = Box::new(1);
inspector = Inspector(&days);
// 如果days碰巧先被销毁了
// 那么当销毁Inspector的时候,它会读取被释放的内存
}

error: `days` does not live long enough
--> <anon>:15:1
|
12 | inspector = Inspector(&days);
| ---- borrow occurs here
...
15 | }
| ^ `days` dropped here while still borrowed
|
= note: values in a scope are dropped in the opposite order they are created
error: aborting due to previous error

实现Drop使得Inspector可以在销毁前执行任意的代码。一些通常认为和它生命周期一样长的类型可能实际上比它先销毁,而这会有潜在的问题。

有意思的是,只有泛型需要考虑这个问题。如果不是泛型的话,那么唯一可用的生命周期就是'static,而它确确实实会永远存在。这也就是这一问题被称之为“安全泛型销毁”的原因。安全泛型销毁是通过drop检查器执行的。我们还未涉及到drop检查器判断类型是否可用的细节,但其实我们之前已经讨论了这个问题的最主要规则:
一个安全地实现Drop的类型,它的泛型参数生命周期必须严格地长于它本身

遵守这一规则(大部分情况下)是满足借用检查器要求的必要条件,同时是满足安全要求的充分非必要条件。也就是说,如果类型遵守上述规则,它就一定可以安全地drop。

之所以并不总是满足借用检查器要求的必要条件,是因为有时类型借用了数据但是在Drop的实现里没有访问这些数据。

例如,上面的Inspector的这一变体就不会访问借用的数据:

struct Inspector<'a>(&'a u8, &'static str);
impl<'a> Drop for Inspector<'a> {
fn drop(&mut self) {
println!("Inspector(_, {}) knows when *not* to inspect.", self.1);
}
}
fn main() {
let (inspector, days);
days = Box::nex(1);
inspector = Inspector(&days, "gadget");
// 假设days碰巧先被销毁。
// 可当Inspector被销毁时,它的析构函数也不会访问借用的days。
}

同样,这个变体也不会访问借用的数据:
use std::fmt;
struct Inspector<T: fmt::Display>(T, &'static str);
impl<T: fmt::Display> Drop for Inspector<T> {
fn drop(&mut self) {
println!("Inspector(_, {}) knows when *not* to inspect.", self.1);
}
}
fn main() {
let (inspector, days): (Inspector<&u8>, Box<u8>);
days = Box::new(1);
inspector = Inspector(&days, "gadget");
// 假设days碰巧先被销毁。
// 可当Inspector被销毁时,它的析构函数也不会访问借用的days。
}

但是,借用检查器在分析main函数的时候会拒绝上面两段代码,并指出days存活得不够长。

这是因为,当借用检查分析main函数的时候,它并不知道每个InspectorDrop实现的内部细节。它只知道inspector的析构函数有访问借用数据的可能。

因此,drop检查器强制要求一个值借用的所有数据的生命周期必须严格长于值本身。

3.9.1 留一个后门

上面的类型检查的规则在未来有可能会松动。

当前的分析方法是很保守甚至苛刻的,它强制要求一个值借用的数据必须比值本身长寿,以保证绝对的安全。

未来的版本中,分析过程会更加精细,以减少安全的代码被拒绝的情况。比如上面的两个Inspector,它们知道在销毁过程中不应该被检查。

同时,有一个还未稳定的属性可以用来(非安全地)声明类型的析构函数保证不会访问过期的数据,即使类型的签名显示有这种可能存在。

这个属性是my_dangle,在RFC 1327中被引入。我们可以这样将其放在上面的Inspector例子里:

struct Inspector<'a>(&'a u8, &'static str);
unsafe impl<#[may_dangle] 'a> Drop for Inspector<'a> {
fn drop(&mut self) {
println!("Inspector(_, {}) knows when *not* to inspect.", self.1);
}
}

使用这个属性要求Drop的实现被标为unsafe,因为编译器将不会检查有没有过期的数据(比如self.0)被访问。

这个属性可以赋给任意数量的生命周期和类型参数。下面这个例子里,我们声明我们不会访问有生命周期'b的引用背后的数据,而类型T也只会被用来转移或销毁。但是我们没有为'aU添加属性,因为我们确实会用到这个生命周期和类型:

use std::fmt::Display;
struct Inspector<'a, 'b, T, U: Display>(&'a u8, &'b u8, T, U);
unsafe impl<'a, #[may_dangle] 'b, #[may_dangle] T, U: Display> Drop for Inspector<'a, 'b, T, U> {
fn drop(&mut self) {
println!("Inspector({}, _, _, {})", self.0, self.3);
}
}

上面的例子中,哪些数据不会被用到是一目了然的。但是,有时候这些泛型参数会被间接地访问。间接访问的形式包括:

  • 使用回调函数
  • 通过调用trait方法
    (在日后的版本里可能增加其他间接访问的途径。)

以下是使用回调的例子:

struct Inspector<T>(T, &'static str, Box<for <'r> fn(&'r T) -> String>);
impl<T> Drop for Inspector<T> {
fn drop(&mut self) {
// 如果T的类型是&'a _,self.2的调用可能访问借用的数据
println!("Inspector({}, {}) unwittingly inspects expired data.",
(self.2)(&self.0), self.1);
}
}

这是trait方法调用的例子:
use std::fmt;
struct Inspector<T: fmt::Display>(T, &'static str);
impl<T: fmt::Display> Drop for Inspector<T> {
fn drop(&mut drop) {
// 下面有一个对<T as Display>::fmt的隐藏调用,
// 当T的类型是&'a _时,可能访问借用数据
println!("Inspector({}, {}) unwittingly inspects expired data.",
self.0, self.1);
}
}

当然,这些访问可以进一步地被隐藏在其他的析构函数调用的方法里,而不仅是直接写在函数中。

上面的几个例子里,&'a u8都在析构函数里被访问了。如果给它添加#[may_dangle]属性,这些类型很可能会产生借用检查器无法捕捉的错误,引发不可预料的灾难。所以最好能避免使用这个属性。

3.10 PhantomData-幽灵数据

在编写非安全代码时,我们常常遇见这种情况:类型或生命周期逻辑上与一个结构体关联起来了,但是却不属于结构体的任何一个成员。这种情况对于生命周期尤为常见。比如,&'a [T]Iter大概是这么定义的:

struct Iter<'a, T: 'a> {
ptr: *const T,
end: *const T,
}

但是,因为'a没有在结构体内被使用,它是无界的。由于一些历史原因,无界生命周期和类型禁止出现在结构体定义中。所以我们必须想办法在结构体内用到这些类型,这也是正确的变性检查和drop检查的必要条件。

我们使用一个特殊的标志类型PhantomData做到这一点。PhantomData不消耗存储空间,它只是模拟了某种类型的数据,以方便静态分析。这么做比显式地告诉类型系统你需要的变性更不容易出错,而且还能提供drop检查需要的信息。

Iter逻辑上包含一系列&'a T,所以我们用PhantomData这样去模拟它:

use std::marker;
struct Iter<'a, T: 'a> {
ptr: *const T,
end: *const T,
_marker: marker::PhantomData<&'a T>,
}

就是这样,生命周期变得有界了,你的迭代器对于’a和T也可变了。一切尽如人意。

另一个重要的例子是Vec,它差不多是这么定义的:

struct Vec<T> {
data: *const T, // *const是可变的!
len: usize,
cap: usize,
}

和之前的例子不同,这个定义已经满足我们的各种要求了。Vec的每一个泛型参数都被至少一个成员使用过了。非常完美!

你高兴的太早了。

Drop检查器会判断Vec<T>并不拥有T类型的值,然后它认为无需担心Vec在析构函数里能不能安全地销毁T,再然后它会允许人们创建不安全的Vec析构函数。

为了让drop检查器知道我们确实拥有T类型的值,也就是需要在销毁Vec的时候同时销毁T,我们需要添加一个额外的PhantomData:

use std::marker:
struct Vec<T> {
data: *const T, // *const是可变的!
len: usize,
cap: usize,
_marker: marker::PhantomData<T>,
}

让裸指针拥有数据是一个很普遍的设计,以至于标准库为它自己创造了一个叫Unique<T>的组件,它可以:

  • 封装一个*const T处理变性
  • 包含一个 PhantomData
  • 自动实现Send/Sync,模拟和包含T时一样的行为
  • 将指针标记为NonZero以便空指针优化
3.10.1 PhantomData模式表

下表展示了各种牛X闪闪的PhantomData用法

Phantom 类型 ‘a ‘T
PhantomData<T> - 协变(可触发drop检查)
PhantomData<&'a T> 协变 协变
PhantomData<&'a mut T> 协变 不变
PhantomData<*const T> - 协变
PhantomData<*mut T> - 不变
PhantomData<fn(T)> - 逆变(*)
PhantomData<fn() -> T - 协变
PhantomData<fn(T) -> T> - 不变
PhantomData<Cell<&'a ()>> 不变 -

(*)如果发生变性的冲突,这个是不变的

3.11 分解借用

可变引用的Mutex属性在处理复合类型时能力非常有限。借用检查器只能理解一些简单的东西,而且极易失败。他对结构体还算是充分了解,知道结构体的成员可能被分别借用。所以这段代码现在可以正常工作:

struct Foo {
a: i32,
b: i32,
c: i32,
}
let mut x = Foo {a: 0, b: 0, c: 0};
let a = &mut x.a;
let b = &mut x.b;
let c = &x.c;
*b += 1;
let c2 = &x.c;
*a += 10;
println!("{} {} {} {}", a, b, c, c2);

但是,借用检查器对于数组和slice的理解却是一团浆糊,所以这段代码无法通过检查:
let mut x = [1, 2, 3];
let a = &mut x[0];
let b = &mut x[1];
println!("{} {}", a, b);

<anon>:4:14: 4:18 error: cannot borrow `x[..]` as mutable more than once at a time
<anon>:4 let b = &mut x[1];
^~~~
<anon>:3:14: 3:18 note: previous borrow of `x[..]` occurs here; the mutable borrow prevents subsequent moves, borrows, or modification of `x[..]` until the borrow ends
<anon>:3 let a = &mut x[0];
^~~~
<anon>:6:2: 6:2 note: previous borrow ends here
<anon>:1 fn main() {
<anon>:2 let mut x = [1, 2, 3];
<anon>:3 let a = &mut x[0];
<anon>:4 let b = &mut x[1];
<anon>:5 println!("{} {}", a, b);
<anon>:6 }
^
error: aborting due to 2 previous errors

借用检查器连这个简单的场景都理解不了,那它更不可能理解一些通用容器类型了,比如说树,尤其是出现不同的键对应相同的值的时候。

为了能“教育”借用检查器我们的所作所为是正确的,我们还是要使用非安全代码。比如,可变slice暴露了一个split_at_mut的方法,它接收一个slice然后返回两个可变slice。一个包括索引值左边所有的值,另一个包含右边所有的值。我们知道这个方法是安全的,因为两个slice没有重叠部分,也就不会出现别名问题。但是它的实现还是要涉及到非安全的内容:

fn split_at_mut(&mut self, mid: usize) -> (&mut [T], &mut [T]) {
let len = self.len();
let ptr = self.as_mut_ptr();
assert!(mid <= len);
unsafe {
(from_raw_parts_mut(ptr, mid)),
from_raw_parts_mut(ptr.offset(mid as isize), len - mid))
}
}

这有一点难懂。为了避免两个&mut指向相同的值,我们通过裸指针显式创建了两个全新的slice。

不过迭代器产生可变引用的方法更加难懂。迭代器trait的定义如下:

trait Iterator {
typr Item;
fn next(&mut self) -> Option<Self::Item>;
}

这份定义里,Self::Itemslef没有直接关系。也就是说我们可以连续调用next很多次,并且同时保存着所有的结果。对于值的迭代器这么做完全可以,完全符合语义。对于共享引用这么做也没什么问题,因为允许任意过个共享引用指向同一个值(当然迭代器本身需要是独立于被共享内容的对象)。

但是可变引用就麻烦了。乍一看,可变引用完全不适用这个API,因为那会产生多个指向相同对象的可变引用。

可实际上它能够正常工作,这是因为迭代器是一个一次性对象。IterMut生成的东西最多只会生成一次,所以实际上我们没有生成多个指向相同数据的可变指针。

更不可思议的是,可变迭代器对于许多类型的实现甚至不需要非安全代码!

例如,下面是单向列表的代码:

type Link<T> = Option<Box<Node<T>>>;
struct Node<T> {
elem: T,
next: Link<T>,
}
pub struct LinkedList<T> {
head: Link<T>,
}
pub struct IterMut<'a, T: 'a>(Option<&'a mut Node<T>>);
impl<T> LinkedList<T> {
fn iter_mut(&mut self) -> IterMut<T> {
IterMut(self.head.as_mut().map(|node| &mut **node))
}
}
impl<'a, T> Iterator for IterMut<'a, T> {
type Item = &'a mut T;
fn next(&mut self) -> Option<Self::Item> {
self.0.take().map(|node| {
self.0 = node.next.as_mut().map(|node| &mut **node);
&mut node.elem
})
}
}

这是可变slice:
use std::mem;
pub struct IterMut<'a, T: 'a>(&'a mut[T]);
impl<'a, T> Iterator for IterMut<'a, T> {
type Item = &'a mut T;
fn next(&mut self) -> Option<Self::Item> {
let slice = mem::replace(&mut self.0, &mut []);
if slice.is_empty() { return None; }
let (l, r) = slice.split_at_mut(1);
self.0 = r;
l.get_mut(0)
}
}
impl<'a, T> DoubleEndedIterator for IterMut<'a, T> {
fn next_back(&mut self) -> Option<Self::Item> {
let slice = mem::replace(&mut self.0, &mut []);
if slice.is_empty() { return None; }
let new_len = slice.len() - 1;
let (l, r) = slice.split_at_mut(new_len);
self.0 = l;
r.get_mut(0)
}
}

还有二叉树:
use std::collections::VecDeque;
type Link<T> = Option<Box<Node<T>>>;
struct Node<T> {
elem: T,
left: Link<T>,
right: Link<T>,
}
pub struct Tree<T> {
root: Link<T>,
}
struct NodeIterMut<'a, T: 'a> {
elem: Option<&'a mut T>,
left: Option<&'a mut Node<T>>,
right: Option<&'a mut Node<T>>,
}
enum State<'a, T: 'a> {
Elem(&'a mut T),
Node(&'a mut Node<T>),
}
pub struct IterMut<'a, T: 'a>(VecDeque<NodeIterMut<'a, T>>);
impl<T> Tree<T> {
pub fn iter_mut(&mut self) -> IterMut<T> {
let mut deque = VecDeque::new();
self.root.as_mut().map(|root| deque.push_front(root.iter_mut()));
IterMut(deque)
}
}
impl<T> Node<T> {
pub fn iter_mut(&mut self) -> NodeIterMut<T> {
NodeIterMut {
elem: Some(&mut self.elem),
left: self.left.as_mut().map(|node| &mut **node),
right: self.right.as_mut().map(|node| &mut **node),
}
}
}
impl<'a, T> Iterator for NodeIterMut<'a, T> {
type Item = State<'a, T>;
fn next(&mut self) -> Option<Self::Item> {
match self.left.take() {
Some(node) => Some(State::Node(node)),
None => match self.elem.take() {
Some(elem) => Some(State::Elem(elem)),
None => match self.right.take() {
Some(node) => Some(State::Node(node)),
None => None,
}
}
}
}
}
impl<'a, T> DoubleEndedIterator for NodeIterMut<'a, T> {
fn next_back(&mut self) -> Option<Self::Item> {
match self.right.take() {
Some(node) => Some(State::Node(node)),
None => match self.elem.take() {
Some(elem) => Some(State::Elem(elem)),
None => match self.left.take() {
Some(node) => Some(State::Node(node)),
None => None,
}
}
}
}
}
impl<'a, T> Iterator for IterMut<'a, T> {
type Item = &'a mut T;
fn next(&mut self) -> Option<Self::Item> {
loop {
match self.0.front_mut().and_then(|node_it| node_it.next()) {
Some(State::Elem(elem)) => return Some(elem),
Some(State::Node(node)) => self.0.push_front(node.iter_mut()),
None => if let None = self.0.pop_front() { return None },
}
}
}
}
impl<'a, T> DoubleEndedIterator for IterMut<'a, T> {
fn next_back(&mut self) -> Option<Self::Item> {
loop {
match self.0.back_mut().and_then(|node_it| node_it.next_back()) {
Some(State::Elem(elem)) => return Some(elem),
Some(State::Node(node)) => self.0.push_back(node.iter_mut()),
None => if let None = self.0.pop_back() { return None },
}
}
}
}

所有这些都是完全安全而且能稳定运行的!这已经超出了我们之前看过的简单结构体的例子:Rust能够理解你把一个可变引用安全地分解为多个部分。接下来我们可以通过Option永久地访问这个引用(或者像对于slice那样,替换为一个空的slice)。