Rust-死灵术2

四、类型转换

4.1 强制类型转换

在一些特定场景中,类型会被隐式地强制转换。这种转换通常导致类型被“弱化”,主要针对指针生命周期。主要目的是让Rust适用于更多的场景,并且基本上是无害的。

强制转换包括下面几种:

如下几种类型之间允许进行强制转换:

  • 传递性:当T_1可以强制转换为T_2T_2可以强制转换为T_3时,T_1就可以强制转换为T_3
  • 指针弱化:
    • &mut T转换为&T
    • *mut T转换为*const T
    • &T转换为*const T
    • &mut T转换为*mut T
  • Unsize:如果T实现了CoerceUnsized<U>,那么T可以强制转换为U
  • 强制解引用:如果T可以解引用为U(比如T: Deref<Target=U>),那么&T类型的表达式&x可以强制转换为&U类型的&*x

所有的指针类型(包括Box和Rc这些智能指针)都实现了CoerceUnsized<Pointer<U>> for Pointer<T> where T: Unsize<U>。Unsize只能被自动实现,并且实现如下转换方式:

  • [T; n] => [T]
  • T => Trait,其中T: Trait
  • Foo<..., T, ...> => Foo<…, U, …>,其中
    • T: Unsize<U>
    • Foo是一个结构体
    • 只有Foo的最后一个成员是和T有关的类型
    • 其他成员的类型与T无关
    • 如果最后一个成员的类型是Bar<T>,那么必须有Bar<T>: Unsize<Bar<U>>

强制转换会在“强制转换位置”处发生。每一个显式声明了类型的位置都会引起到该类型的强制转换。但如果必须进行类型推断,则不会发生类型转换。表达式e到类型U的强制转换位置包括:

  • let 表达式,静态变量或者常量:let x: U = e
  • 函数的参数:takes_a_U(e)
  • 函数返回值:fn foo() -> U {e}
  • 结构体初始化:Foo { some_u: e }
  • 数组初始化:let x: [U; 10] = [e, ...]
  • 元组初始化:let x: (U, ..) = (e, ..)
  • 代码块中的最后一个表达式:let x: U = { ..; e }

注意,在匹配trait的时候不会发生强制类型转换(receiver除外,具体见下)。也就是说,如果为U实现了一个trait,T可以强制转换为U,并不能认为T也实现了这个trait。例如,下面的代码无法通过类型检查,虽然t可以强制转换为&T,而且有一个&T的trait实现。

trait Trait {}
fn foo<X: Trait>(t: X) {}
impl<'a> Trait for &'a i32 {}
fn main() {
let t: &mut i32 = &mut 0;
foo(t);
}

<anon>:10:5: 10:8 error: the trait bound `&mut i32 : Trait` is not satisfied [E0277]
<anon>:10 foo(t);
^~~

4.2 点操作符

点操作符可以做到许多神奇的类型转换任务,比如自动引用,自动解引用,还有级联类型匹配后的强制类型转换。

4.3 显式类型转换

显式类型转换是强制类型转换的超集:所有的强制类型转换都可以通过显式转换的方式主动触发。但有一些场景只适用于显式转换。强制类型转换很普遍而且通常无害,但是显式类型转换是一种“真正的转换“,它的应用就很稀少了,而且有潜在的危险。因此,显式转换必须通过关键字as主动地触发:expr as Type

真正的转换一般是针对裸指针和基本数字类型的。虽然说过它们存在风险,但是在运行期却是很稳定的。如果类型转换操作触发了一些奇怪的边界场景,Rust并不会给出任何提示。转换仍然会被认为是成功的。这就要求显式类型转换必须在类型层面是合法的,否则会在编译期被拒绝。比如,7u8 as bool不会编译成功。

也就是说,显式类型转换不属于非安全(unsafe)行为,因为仅凭转换操作是不会违背内存安全性的。比如,将整型转换为裸指针很容易导致可怕的后果。但是,创建一个指针这个行为本身是安全的,而真正使用裸指针的操作则必须被标为unsafe

以下是所有显式类型转换的情况。简单起见,我们用*表示*const或者*mut,用integer表示任意整数基本类型:

  • *T as *U,其中T, U: Sized
  • *T as *U,明确unsize的情况
  • *T as integer
  • integer as *T
  • number as number
  • 无成员枚举as integer
  • bool as integer
  • char as integer
  • u8 as char
  • &[T; n] as *const T
  • fn as *T,其中T: Sized
  • fn as integer

注意,裸slice转换后长度会改变,比如*const [u16] as *const [u8]创建的slice只包含原本一半的内存。

显示类型转换不是可传递的,也就是说,即使e as U1 as U2是合法的表达式,也不能认为e as U2就一定是合法的。

对于数字类型的转换,如下几点需要注意:

  • 相同大小的整型互相转换(比如i32->u32)是一个no-op
  • 大尺寸的整型转换为小尺寸的整型(比如u32->u8)会被截断
  • 小尺寸的整型转换为大尺寸的整型(比如u8->u32
    • 如果源类型是无符号的,将会补零
    • 如果源类型是有符号的,将会有符号补零
  • 浮点类型转换为整型会舍去浮点部分
    • 注意:如果目标整数类型不能表示舍入的结果,在目前这是一个未定义行为。包括Inf和NaN。这是一个bug,会在后续版本中修复。
  • 整型转换为浮点类型会产生这个整型的浮点型表示,
  • f32转换为f64可以无损失地完美转换,必要的时候做舍入(舍入到最近的可能取值,距离相同的取偶数)
  • f64转换为f32会生成最近可能值(舍入到最近的可能取值,距离相同的取偶数)

4.4 变形

虽然本书都是关于非安全的内容,我还是希望你能仔细考虑避免使用本章讲到的内容。这是你在Rust中所能做到的真真正正、彻彻底底、最最可怕的非安全行为。所有的保护机制都形同虚设。

mem::transmute<T, U>接受一个T类型的值,然后将它重新解析为类型U。唯一的限制是TU必须有同样的大小。可能产生未定义行为的情况让人看着头疼。

  • 最重要的,创建任一类型的处于不合法状态的示例,都将产生不可预知的混乱
  • transmute有一个重载的返回类型。如果没有明确指定返回类型,它会返回一个满足类型推断的奇怪类型
  • 使用不合法的值构建基本类型是未定义行为
  • 非repr(C)的类型之间相互变形是未定义行为
  • &变形为&mut是未定义行为
    • &变形为&mut永远都是未定义行为
    • 不要多想,你绝对不能这么做
    • 不要多想,你没有什么特殊的
  • 变形为一个未指定生命周期的引用会产生无界生命周期

mem::transmute_copy<T, U>很神奇地比这更加不安全。它从&T拷贝size_of<U>个字节并将它们解析为Umem::transmute仅有的类型大小的检查都不见了(因为拷贝类型前缀有可能是合法的),只不过U的尺寸比T大会被视为一个未定义行为。

五、未初始化内存

5.1 安全方式

和C一样,所有栈上的变量在显式赋值之前都是未初始化的。而和C不同的是,Rust禁止你在赋值之前读取它们:

fn main() {
let x: i32;
println!("{}", x);
}

src/main.rs:3:20: 3:21 error: use of possibly uninitialized variable: `x`
src/main.rs:3 println!("{}", x);
^

这个错误基于分支分析:任何一个分支在第一次使用x之前都必须对它赋值。有意思的是,如果每一个分支都只赋值一次的话,Rust并不要求变量是可变的。但是,这个分析过程没有配合常量分析。所以下面这段代码可以编译:
fn main() {
let x: i32;
if true {
x = 1;
} else {
x = 2;
}
println!("{}", x);
}

但是这段却不能编译:
fn main() {
let x: i32;
if true {
x = 1;
}
println!("{}", x);
}

src/main.rs:6:17: 6:18 error: use of possibly uninitialized variable: `x`
src/main.rs:6 println!("{}", x);

而这一段又可以编译:
fn main() {
let x: i32;
if true {
x = 1;
println!("{}", x);
}
// 不关心其他的未初始化变量的分支
// 因为我们并不使用那些分支
}

当然,虽然分析过程不知道变量的实际值,它对依赖和控制流程的理解还是比较深入的。比如,这段代码是正确的:
let x: i32;
loop {
// Rust不知道这个分支会被无条件执行
//因为它依赖于实际值
if true {
// 但是它确实知道循环只会有一次,因为我们会无条件break
// 所以x不需要是可变的
x = 0;
break;
}
}
// 它也知道如果没有执行break的话,代码不会运行到这里
// 所以在这里x一定已经被初始化了
println!("{}", x);

如果值从变量中移出且变量类型不是Copy,那么变量逻辑上处于未初始化状态。就是说:
fn main() {
let x = 0;
let y = Box::new(0);
let z1 = x; // x仍然是合法的,因为i32是Copy
let z2 = y; // y现在逻辑上未初始化,因为Box不是Copy
}

但是,这个例子中对y重新赋值要求y是可变的,因为安全Rust能够观察到y的值发生了变化:
fn main() {
let mut y = Box::new(0);
let z = y; // y现在逻辑上未初始化,因为Box不是Copy
y = Box::new(1); // 重新初始化y
}

否则y会被视为一个全新的变量。

5.2 Drop标志

对于Copy类型,这一点不是很重要,因为数据不过是一堆字节而已。但是对于有析构函数的类型就是另外一回事了:变量每次被赋值或者离开作用域的时候,Rust都需要判断是否要调用析构函数。在有条件地初始化的情况下,Rust是如何做到这一点的呢?

注意,不是所有的赋值操作都需要考虑这一点。通过解引用赋值是一定会触发析构函数,而使用let赋值则一定不会触发:

let mut x = Box::new(0); // let传建一个全新的变量,所以一定不会调用drop
let y = &mut x;
*y = Box::new(1); // 解引用假设被引用变量是初始化过的,所以一定会调用drop

只有当覆盖一个已经初始化的变量或者变量的一个子成员时,才需要考虑这个问题。

Rust实际上是在运行期判断是否销毁变量。当一个变量被初始化和反初始化时,变量会更新它的”drop标志“的状态。通过解析这个标志的值,判断变量是否真的需要执行drop。

当然,大多数情况下,在编译期就可以知道一个值在每一点的初始化状态。符合这一点的话,编译器理论上可以生成更有效率的代码!比如,无分支的程序有着如下的静态drop语义:

let mut x = Box::new(0); // x未初始化;仅覆盖值
let mut y = x; // y未初始化;仅覆盖值,并设置x为未初始化
x = Box::new(0); // x未初始化;仅覆盖值
y = x; // y已初始化;销毁y,覆盖它的值,设置x为未初始化
// y离开作用域;y已初始化;销毁y
// x离开作用域;x未初始化;什么都不用做

类似的,有分支的代码当所有分支中的初始化行为一致的时候,也可以有静态的drop语义:
let mut x = Box::new(0); // x未初始化;仅覆盖值
if condition {
drop(x); // x失去值;设置x为未初始化
} else {
printn!("{}", x);
drop(x); // x失去值;设置x为未初始化
}
x = Box::new(0); // x未初始化;仅覆盖值
// x离开作用域;x已初始化;销毁x

但是,下面的代码则需要运行时信息以正确执行drop:
let x;
if condition {
x = Box::new(0); // x未初始化;仅覆盖值
println!("{}", x);
}
// x离开作用域;x可能未初始化
// 检查drop标志

当然,修改为下面的代码就又可以得到静态drop语义:
if condition {
let x = Box::new(0);
println!("{}", x);
}

5.3 非安全方式

一个特殊情况是数组。安全Rust不允许部分地初始化数组。初始化一个数组时,你可以通过let x = [val; N]为每一个位置赋予相同的值,或者是单独指定每一个成员的值let x = [val1, val2, val3]。不幸的是,这个要求太苛刻了。很多时候我们需要用增量或者动态的方式初始化数组。

非安全Rust给我们提供了一个很有力的工具以处理这一问题:mem::uninitialized。这个函数假装返回一个值,但其实它什么也没有做。我们用它来欺骗Rust我们已经初始化了一个变量了,从而可以做一些很神奇的事情,比如有条件还有增量地初始化

不过,它也给我们打开了各种问题的大门。在Rust中,对于已初始化和未初始化的变量赋值,是有不同的含义的。如果Rust认为变量未初始化,它会将字节拷贝到未初始化的内存区域,别的就什么都不做了。可如果Rust判断变量已初始化,它会销毁原有的值!因为我们欺骗Rust值已经初始化,我们再也不能安全地赋值了。

系统分配器返回一个指向未初始化内存的指针,与它配合时同样会造成问题。

接下来,我们还必须使用ptr模块。特别是它提供的三个函数,允许我们将字节码写入一块内存而不会销毁原有的变量。这些函数为:writecopycopy_nonoverlapping

  • ptr::write(ptr, val)函数接受val然后将它的值移入ptr指向的地址
  • ptr::copy(src, dest, count)函数从src处将countT占用的字节拷贝到dest。(这个函数和memmove相同,不过要注意参数顺序是反的!)
  • ptr::copy_nonoverlapping(src, dest, count)copy的功能是一样的,不过它假设两段内存不会有重合部分,因此速度会略快一点。(这个函数和memcpy相同,不过要注意参数顺序是反的!)

显然,如果这些函数被滥用的话,很可能导致错误或者未定义行为。它们唯一的要求就是被读写的位置必须已经分配了内存。但是,向任意位置写入任意字节很可能造成不可预测的错误。

下面的代码集中展示了它们的用法:

use std::mem;
use std::ptr;
// 数组的大小是硬编码的但是可以很方便地修改
// 不过这表示我们不能用[a, b, c]这种方式初始化数组
const SIZE: usize = 10;
let mut x: [Box<u32>; SIZE];
unsafe {
// 欺骗Rust说x已经被初始化
x = mem::uninitialized();
for i in 0..SIZE {
// 十分小心地覆盖每一个索引值而不读取它
// 注意:异常安全性不需要考虑;Box不会panic
ptr::write(&mut x[i], Box::new(i as u32));
}
}
println!("{:?}", x);

需要注意,你不用担心ptr::write和实现了Drop的或者包含Drop子类型的类型之间无法和谐共处,因为Rust知道这时不会调用drop。类似的,你可以给一个只有局部初始化的结构体的成员赋值,只要那个成员不包含Drop子类型。

但是,在使用未初始化内存的时候你需要时刻小心,Rust可能会在值未完全初始化的时候就尝试销毁它们。如果一个变量有析构函数,那么变量作用域的每一个代码分支都应该在结束之前完成变量的初始化。否则会导致崩溃。

这就是未初始化内存的全部内容!其他地方基本上不会再涉及到未初始化内存了,所以如果你想跳过本章,请千万小心。

六、基于所有权的资源管理

OBRM(又被成为RAII:Resource Acquisition is Initialization,资源获取即初始化),在Rust中你会有很多和它打交道的机会,特别是在使用标准库的时候。

这个模式简单来说是这样的:如果要获取资源,你只要创建一个管理它的对象。如果要释放资源,你只要销毁这个对象,由对象负责为你回收资源。而所谓资源通常指的就是内存。Box,Rc,以及std::collections中几乎所有的东西都是为了方便且正确地管理内存而存在的。这对于Rust尤为重要,因为我们并没有垃圾回收器帮我们管理内存。关键点就在这:Rust要掌控一切。不过我们并不是只能管理内存。差不多所有的系统资源,比如线程、文件、还有socket,都可以用到这些API。

6.1 构造函数

创建一个自定义类型的实例的方法只有一种:先命名,然后一次性初始化它的所有成员:

struct Foo {
a: u8,
b:u32,
c: bool,
}
enum Bar {
X(u32),
Y(bool),
}
struct Unit;
let foo = Foo { a: 0, b: 1, c: false };
let bar = Bar::X(0);
let empty = Unit;

就是这样。其他的所谓创建类型实例的方式,不过是调用一些函数,而函数的底层还是要依赖于这个真正的构造函数。

和C++不同,Rust没有很多不同种类的构造函数,比如拷贝、默认、赋值、移动、还有其他各种构造函数。之所以这样的原因有很多,不过归根结底还是因为Rust显式化的设计哲学。

移动构造函数对于Rust没什么用,因为我们并不需要让类型关心它们在内存上的位置。每一个类型都有可能随时被memcopy到内存中其他的位置上。这也意味和那种存储于栈上却依然可以移动的侵入式链表在Rust中是不可能(安全地)存在的。

复制和拷贝构造函数也是不存在的,因为Rust中的类型有且仅有移动语义。x = y只是将y的字节移动到x的变量中。Rust倒是提供了两种和C++中的copy语义相似的功能:CopyCloneClone很像是拷贝构造函数,但是它不会被隐式调用。你必须在需要复制的元素上显式调用clone方法、CopyClone的一个特例,它的实现只会拷贝字节码Copy类型在移动的时候会隐式地复制,但是因为Copy的定义,这个方法只是不把旧的值设置为未初始化而已——其实是一个no-op。

虽然Rust确实有一个Default trait,它与默认构造函数很相似,但是这个trait极少被用到。这是因为变量不会被隐式初始化。Default一般只有在泛型编程中才有用。而具体的类型会提供一个new静态方法来实现默认构造函数的功能。这个和其他语言中的new关键字没什么关系,也没有什么特殊的含义。它仅仅是一个明明习惯而已。

6.2 析构函数

Rust通过Drop trait提供了一个成熟的自动析构函数,包含了这个方法:

fn drop(&mut self);

这个方法给了类型一个彻底完成工作的机会。

drop执行之后,Rust会递归地销毁self的所有成员

这个功能很方便,你不需要每次都写一堆重复的代码来销毁子类型。如果一个结构体在销毁的时候,除了销毁子成员之外不需要做什么特殊的操作,那么它其实可以不用实现Drop

在Rust 1.0中,没有什么合适的方法可以打断这个过程。

注意,参数是&mut self意味着即使你可以阻止递归销毁,Rust也不允许你将子成员的所有权移出。对于大多数类型来说,这一点完全没问题。

比如,一个自定义的Box的实现,它的Drop可能长这样:

#![feature(ptr_internals, allocator_api)]
use std::alloc::{Alloc, Global, GlobalAlloc, Layout};
use std::mem;
use std::ptr::{drop_in_place, NonNull, Unique};
struct Box<T>{ ptf: Unique<T> }
impl<T> Drop for Box<T> {
fn drop(&mut self) {
unsafe {
drop_in_place(self.ptr.as_ptr());
let c: NonNull<T> = self.ptr.into();
Global.dealloc(c.cast(), Layout::new::<T>())
}
}
}

这段代码是正确的,因为当Rust要销毁ptr的时候,它见到的是一个Unique,没有Drop的实现。类似的,也没有人能在销毁后再使用ptr,因为drop函数退出之后,他就不可见了。

可是这段代码是错误的:

#![feature(allocator_api, ptr_internals)]
use std::alloc::{Alloc, Global, GlobalAlloc, Layout};
use std::ptr::{drop_in_place, Unique, NonNull};
use std::mem;
struct Box<T> { ptr: Unique<T> }
impl<T> Drop for Box<T> {
fn drop(&mut self) {
unsafe {
drop_in_place(self.ptr.as_ptr());
let c: NonNull<T> = self.ptr.into();
Global.dealloc(c.cast(), LayOut::new::<T>());
}
}
}
struct SuperBox<T> ( my_box: Box<T> )
impl<T> Drop for SuperBox<T> {
fn drop(&mut self) {
// 回收box的内容,而不是drop它的内容
let c: NonNull<T> = self.my_box.ptr.into();
Global.dealloc(c.cast::<u8>(), LayOut::new::<T>());
}
}

当我们在SuperBox的析构函数里回收了boxptr之后,Rust会继续让box销毁它自己,这时销毁后使用(use-after-free)和两次释放(double-free)的问题立刻接踵而至,摧毁一切。

注意,递归销毁适用于所有的结构体和枚举类型,不管它有没有实现Drop。所以,这段代码

struct Boxy<T> {
data1: Box<T>,
data2: Box<T>,
info: u32,
}

在销毁的时候也会调用data1data2的析构函数,尽管这个结构体本身并没有实现Drop。这样的类型“需要Drop却不是Drop”。

类似的

enum Link {
Next(Box<Link>),
None,
}

当(且仅当)一个实例储存着Next变量时,它就会销毁内部的Box成员。

一般来说这其实是一个很好的设计,它让你在重构数据布局的时候无需费心添加/删除drop函数。但也有很多的场景要求我们必须在析构函数中玩一些花招。

如果想阻止递归销毁并且在drop过程中将self的所有权移出,通常的安全的做法是使用Option

#![feature(allocator_api, ptr_internals)]
use std::alloc::{Alloc, GlobalAlloc, Global, LayOut};
use std::ptr::{drop_in_place, Unique, NonNull};
use std::mem;
struct Box<T>{ ptr: Unique<T> }
impl<T> Drop for Box<T> {
fn drop(&mut self) {
unsafe {
drop_in_place(self.ptr.as_ptr());
let c: NonNull<T> = self.ptr.into();
Global.dealloc(c.cast(), LayOut::new::<T>());
}
}
}
struct SuperBox<T> { my_box: Option<Box<T>> }
impl<T> Drop for SuperBox<T> {
fn drop(&mut self) {
unsafe {
// 回收box的内容,而不是drop它的内容
// 需要将box设置为None,以阻止Rust销毁它
let my_box = self.my_box.take().unwrap();
let c: NonNull<T> = my_box.ptr.into();
Global.dealloc(c.cast(), LayOut::new::<T>());
mem::feorget(my_box);
}
}
}

但是这段代码显得很奇怪:我们认为一个永远都是Some的成员有可能是None,仅仅因为析构函数中用到了一次。但反过来说这种设计又很合理:你可以在析构函数中调用self的任意方法。在成员被反初始化之后就完全不能这么做了,而不是禁止你搞出一些随意的非法状态。(斜体部分没看懂,建议看原文)

权衡之后,这是一个可以接受的方案。你可以将它作为你的默认选项。但是,我们希望以后能有一个方法明确声明哪一个成员不会自动销毁。

6.3 泄漏

基于所有权的资源管理是为了简化复合类型而存在的。你在创建对象的时候获取资源,在销毁对象的时候释放资源。由于析构过程做了处理,你不可能忘记释放资源,而且是尽可能早地释放资源!这简直是一个完美的方案,解决了我们所有的问题。

可实际上可怕的事情遍地都是,我们还有新的奇怪的问题需要解决。

许多人觉得Rust已经消除了资源泄露的可能性。实际应用中也差不多是这样。你不太可能看到安全Rust出现不可控制的资源泄露。

但是,从理论的角度来说,情况却完全不同。在科学家看来,“泄露”太过于抽象,根本无法避免。很可能就会有人在程序的开头初始化一个集合,塞进去一大堆带析构函数的对象,接下来就进入一个死循环,再也不理开始的那个集合。那个集合就只能坐在那里无所事事,死死地抱着宝贵的资源等着程序结束(这时操作系统会强制回收资源)。

我们可能要给泄露一个更严格的定义:无法销毁不可达(unreachable)的值。Rust也不能避免这种泄露。事实上Rust还有一个制造泄露的函数:mem::forget。这个函数获取传给它的值,但是不调用它的析构函数。

mem::forget曾经被标为unsafe,作为不要滥用它的一种警告。毕竟不调用析构函数一般来说不是一个好习惯(尽管在某些特殊情况下很有用)。但其实这个判断比较不靠谱,因为在安全代码中不调用析构函数的情况很多。最经典的例子是一个循环引用的计数引用。

安全代码可以合理假设析构函数泄露是不存在的,因为任何有这一问题的程序都可能是错误的。但是,非安全代码不能依赖于运行析构函数来保证程序安全。对于大多数类型而言,这一点不成问题:如果不能调用析构函数,那其实类型本身也是不可访问的,所以这就不是个问题了,对吧?比如,你没有释放Box<u8>,那么你会浪费一点内存,但是这并不会违反内存安全性。

但是对于代理类型,我们就要十分小心它的析构函数了。有几个类型可以访问一个对象,却不拥有对象的所有权。代理类型很少见,而需要你特别小心的类型就更稀少了。但是,我们要仔细研究一下标准库中的三个有意思的例子

  • Vec::Drain
  • Rc
  • thread::scoped::JoinGuard
6.3.1 Drain

drain是一个集合API,它将容器内的数据所有权移出,却不占有容器本身。我们可以声明一个Vec所有内容的所有权,然后复用分配给它的空间。它产生一个迭代器(Drain),以返回Vec的所有值。

现在,假设Drain正迭代到一半:有一些值被移出,还有一些没移出。这表明Vec里有一堆逻辑上未初始化的数据!我们可以在删除值的时候在Vec里再备份一份,但这种方法的性能是不可忍受的。

实际上,我们希望Drain在销毁的时候能够修复Vec的后台存储。他要备份那些没有被移除的元素(drain支持子范围),然后修改Veclen。这种方法甚至还是unwinding安全的!完美!

看看下面这段代码

let mut vec = vec![Box::new(0); e];
{
// 开始drain,vec无法再被访问
let mut drainer = vec.drain(..);
// 移除两个元素,然后立刻销毁他们
drainer.next();
drainer.next();
// 销毁drainer,但是不调用它的析构函数
mem::forget(drainer);
}
// 不好,vec[0]已经被销毁了,我们在读一块释放后的内存
println!("{}", vec[0]);

这个显然很不好。我们现在陷入了两难的境地:保证每一步产生一致的状态,需要付出巨大的性能代价(抵消掉了API带来的所有好处);而不保证一致状态则会在安全代码中产生未定义行为(使API失去稳定性)。

那我们能做什么呢?我们采用一种简单粗暴的方式保证状态一致性:开始迭代的时候就设置Vec的长度为0,然后在析构函数里根据需要再恢复。这样做,在一切正常的情况下,我们可以用最小的代价获得正确的行为。但是,如果有人就是不管不顾地在迭代中间mem::forget,那么结果就是泄露或者更坏(还可能让Vec处于一种虽然一致但实际上不正确的状态)。由于我们认为mem::forget是安全地,那么这种行为也是安全地。我们把造成更多泄露的泄露叫做泄露扩大化(leak amplification)。

6.3.2 Rc

Rc 的情况很有意思,第一眼看上去它根本不像是一个代理类型。毕竟,它自己管理着它指向的数据,并且在销毁Rc的时候也会同时销毁数据的值。泄露Rc的数据好像并不怎么危险。那会让引用计数持续增长,而数据不会被释放或销毁。这和Box的行为是一项的,对吧?

并不是。

我们看一下这个Rc的简单实现:

struct Rc<T> {
ptr: *mut RcBox<T>,
}
struct RcBox<T> {
data: T,
ref_count: usize,
}
impl<T> Rc<T> {
fn new(data: T) -> Self {
unsafe {
// 如果heap::allocate是这样的不是很好嘛?
let ptr = heap::allocate::<RcBox<T>>();
ptr::write(ptr, RcBox {
data: data,
ref_count: 1,
});
Rc { ptr: ptr }
}
}
fn clone(&self) -> Self {
unsafe {
(*self.ptr).ref_count += 1;
Rc { ptr: self.ptr }
}
}
}
impl<T> Drop for Rc<T> {
fn drop(&mut self) {
unsafe {
(*self.ptr).ref_count -= 1;
if (*self.ptr).ref_count == 0 {
// 销毁数据然后释放空间
ptr::read(self.ptr);
heap::deallocate(self.ptr);
}
}
}
}

要解决这个问题,我们可以检查ref_count并根据情况做一些处理。标准库的做法是直接废弃对象,因为这种情况下你的程序进入了一种非常危险的状态。当然,这是一个十分诡异的边界场景。

6.3.3 thread::scoped::JoinGuard

thread::scoped可以保证父线程在共享数据离开作用域之前join子线程,通过这种方式子线程可以引用父线程栈中的数据而不需要做什么同步操作。

pub fn scoped<'a, F>(f: F) -> JoinGuard<'a>
where F: FnOnce() + Send + 'a

这里f是供其他线程执行的闭包。F: Send + 'a表示闭包引用数据的生命周期是'a,而且它可能拥有这个数据或者数据是一个Sync(说明&data是Send)。

因为JoinGuard有生命周期,它所用到的数据都是从父线程里借用的。这意味着JoinGuard不能比线程使用的数据存活更长。当JoinGuard被销毁的时候它会阻塞父线程,保在父线程中被引用的数据离开作用域之前子线程都已经终止了。

用法是这样的:

let mut data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
{
let guards = vec![];
for x in &mut data {
// 将可变引用移入闭包,然后再另外一个线程里执行它
// 闭包有生命周期,其界限由可变引用x的生命周期决定
// 返回的guard也和闭包有相同的生命周期,所以它也和x一样可变借用了data
// 这意味着在guard销毁前我们不能访问data
let guard = thread::scoped(move || {
*x *= 2;
});
// 储存线程的guard供后面使用
guards.push(guard);
}
// 所有的guard在这里被销毁,强制线程join(主线程阻塞在这里等待其他线程终止)。
// 等到线程join后,数据的借用就过期了,数据又可以在主线程中被访问了
}
// 数据在这里已经完全改变了。

这个似乎完全能够正常工作!Rust的所有权系统完美地保证了这一点!……不过这一切的前提是析构函数必须被调用。
let mut data = Box::new(0);
{
let guard = thread::scoped(|| {
// 好一点的情况是这里会有数据竞争
// 最坏的情况是这里会有释放后应用(use-after-free)
*data += 1;
});
// 因为guard被forget了,线程不会阻塞
mem::forget(guard);
}
// Box在这里被销毁,而子线程可能会也可能不会在这里访问数据。

Duang!保证析构函数能运行是这个api的基础,上面这段代码需要一个全新的设计才行。

七、展开

Rust有一个分层的错误处理体系:

  • 如果有些值可以为空,就用Option
  • 如果发生了错误,而错误可以被正常处理,就用Result
  • 如果发生了错误,但是没办法正常处理,就让线程panic
  • 如果发生了更严重的问题,中止(abort)程序

Panic只能被任务的所有者捕获,而捕获后必须立即对它进行相应处理,否则任务会自己停止。

展开(unwinding)在这种场景下十分重要,因为如果任务的析构函数没有被调用的话,会导致内存和其他系统资源的泄露。由于任务有可能在正常运行过程中就挂掉,它对于需要长期运行的系统很不友好。

7.1 异常安全性

如果你对None调用unwrap、使用超出范围的索引值、或者用0做除数,你的程序就要panic。在debug模式下,所有的计算操作在溢出的时候也都会panic。除非你十分小心并且严格控制着每一条代码的行为,否则所有的东西都有展开的可能,你需要时刻准备迎接它。

在更广大的程序设计世界里,应对展开这件事通常被称之为“异常安全“。在Rust中,我们需要考虑两个层次的异常安全性:

  • 在非安全代码中,异常安全的下限是要保证不能违背内存安全性。我们称之为最小异常安全性。
  • 在安全代码中,异常安全性要保证程序时刻在做正确的事情。我们称之为最大异常安全性。

在许多情况下,非安全代码在处理展开的时候需要考虑到那些写得很糟糕的安全代码。一些只是暂时导致不稳定状态的程序需要小心,一旦触发了Panic会导致这种状态无法使用。这表示在不稳定状态依然存在的情况下,我们需要保证值运行不触发Panic的代码;或者在触发Panic的时候即使处理,清除这种状态。这也表明Panic看到的状态并不一定非得是连续的状态,我们只需要保证它是安全地状态就可以。

大多数非安全代码都比较容易实现异常安全。因为它控制着程序运行的每个细节,而且大部分代码不会Panic。但是非安全代码也经常要做诸如在未初始化数据的数组上反复运行外部代码这样的操作。这种代码就需要小心考虑异常安全性了。

7.1.1 Vec::push_all

Vec::push_all使用一个slice扩充Vec,由于它没有具体化类型,所以能获得较高的效率。下面是一个简单的实现:

impl<T: Clone> Vec<T> {
fn push_all(&mut self, to_push: &[T]) {
self.reserve(to_push.len());
unsafe {
// 因为我们调用了reserve,所以不会出现溢出
self.set_len(self.len() + to_push.len());
for (i, x) in to_push.iter().enumerate() {
self.ptr().offset(i as isize).write(x.clone());
}
}
}
}

我们不去使用push,因为它会对Vec的容量和len做额外的检查,而有些情况下我们能够明确知道容量是充足的。这段代码的逻辑是完全正确的,但是却有一个问题:它不是异常安全的!set_lenoffsetwrite都没问题,但是clone是一颗引发Panic的炸弹。

Clone的实现是我们无法控制的,它很可能会panic。如果它真的panic了,这个方法会提前退出,但我们之前给Vec设置的更大的长度会一致保持下去。当Vec被访问或者销毁的时候,它会读取未初始化内存!

解决方法很简单。如果我们要保证我们clone的值都被销毁了,我们可以在每一次循环里设置len。如果我们只是想保证不会出现读取未初始化内存的情况,我们可以在循环之后设置len

7.1.2 BinaryHeap::sift_up

对二叉堆做冒泡比扩充一个Vec要更复杂一点。伪代码是这样的:

bubble_up(heap, index):
while index != 0 && heap[index] < heap[parent(index)]:
heap.swap(index, parent(index))
index = parent(index)

将它翻译成Rust很容易,但是性能不会让人满意:self元素要一遍一遍做无意义的交换。我们更喜欢下面的版本:
bubble_up(heap, index):
let elem = heap[index]
while index != 0 && elem < heap[parent(index)]:
heap[index] = heap[parent(index)]
index = parent(index)
heap[index] = elem

这段代码保证各个元素被尽量少的复制(通常每个元素需要被复制两次)。但是这样它会引发异常安全问题!任何时刻都存在着一个值的两份拷贝。如果这个方法中出现panic,有一些东西可能会被二次释放。不幸的是,我们同样不能完全掌控这段代码,因为比较操作是用户定义的。

这个解决方案比Vec的要困难。一个选项是把用户定义代码和非安全代码拆分成两个阶段:

bubble_up(heap, index):
let end_index = index;
while end_index != 0 && heap[end_index] < heap[parent(end_index)]:
end_index = parent(end_index)
let elem = heap[index]
while index != end_index:
heap[index] = heap[parent(index)]
index = parent(index)
heap[index] = elem

如果用户定义的代码爆炸了,也不会伤及无辜,因为我们还没有实际改变堆的状态。等我们开始在堆上搞事情的时候,我们只会使用我们信任的数据和函数,不用担心panic。

你可能对这个设计感到很不爽。这个属于作弊!而且我们必须对堆完整遍历两次!好吧,让我们直面困难,把不信任代码和不安全代码混合在一起。

如果Rust像Java一样有tryfinally,我们可以这么做:

bubble_up(heap, index):
let elem = heap[index]
try:
while index != 0 && elem < heap[parent(index)]:
heap[index] = heap[parent(index)]
index = parent(index)
finally:
heap[index] = elem

基本思想很简单:如果比较操作panic了,我们就把取出的元素塞回到逻辑上未初始化的位置然后退出。访问这个堆的人可能会发现堆的状态是不连续的,但是至少这个方案不会引发二次释放!如果算法正常结束的话,这个设计就和我们最开始不做任何处理的方案一模一样了。

可惜,Rust并没有这些东西,所以我们只能自己早轮子了!我们把算法的状态储存在一个独立的结构体中,结构体的析构函数起到了”finally“的功能。不管有没有panic,析构函数都会被调用并且清除我们留下状态。

struct Hole<'a, T: 'a> {
data: &'a mut [T],
// elt从始至终都会是Some
elt: Option<T>,
pos: usize,
}
impl<'a, T> Hole<'a, T> {
fn new(data: &'a mut [T], pos: usize) -> Self {
unsafe {
let elt = ptr::read(&data[pos]);
Hole {
data: data,
elt: Some(elt),
pos: pos,
}
}
}
fn pos(&self) -> usize { self.pos }
fn removed(&self) -> &T { self.elt.as_ref().unwrap() }
unsafe fn get(&self, index: usize) -> &T { &self.data[index] }
unsafe fn move_to(&mut self, index: usize) {
let index_ptr: *const _ = &self.data[index];
let hole_ptr = &mut self.data[self.pos];
ptr::copy_nonoverlapping(index_ptr, hole_ptr, 1);
self.pos = index;
}
}
impl<'a, T> Drop for Hole<'a, T> {
fn drop(&mut self) {
// 再次填充hole
unsafe {
let pos = self.pos;
ptr::write(&mut self.data[pos], self.elt.take().unwrap());
}
}
}
impl<T: Ord> BinaryHeap<T> {
fn sift_up(&mut self, pos: usize) {
unsafe {
// 取出pos处的值,然后创建一个hole
let mut hole = Hole::new(&mut self.data, pos);
while hole.pos() != 0 {
let parent = parent(hole.pos());
if hole.removed() <= hole.get(parent) { break }
hole.move_to(parent);
}
// 无论有没有panic,hold在此处都会无条件地被填充
}
}
}

7.2 污染

所有的非安全代码都必须保证最小异常安全性,但是并不是所有的类型都能保证最大异常安全性。即使一个类型保证了这一点,我们的代码也可能把它搞乱。比如,一个整数类型肯定是异常安全的,但是它自己没有语义。而一段代码可能在panic的时候没有正确更新整数的值,因此导致了不连续的状态。

这种情况通常没什么大不了的,因为异常发生时所有的东西都应该被销毁。例如,你给一个线程传递了一个Vec而线程panic了,这时Vec处于奇怪的状态其实也无所谓。反正它会被销毁掉并且永远消失。但是,一些类型会在发生panic的时候偷偷隐藏数据的值。

这些类型在遇到panic的时候可能会污染(poison)自己。污染没有什么特殊的含义,它通常只是指禁止其他人正常地使用它。最明显的例子是标准库中的Mutex类型。Mutex会在它的一个MutexGuards(Mutex在获取锁的时候返回的对象)因为panic而销毁的时候污染自己,这之后所有尝试给Mutex上锁的操作都会返回Err或者Panic

从Rust惯常的角度看,Mutex的污染不算真正地保障安全性。污染是一种守护机制,在Mutex上锁期间遇到Panic后,禁止访问里面的数据。这种数据可能正被修改了一半,处于一种不连续或者不完整的状态。需要注意,只要数据正常写入了,即使使用这种类型也不会违反内存安全性。毕竟,这是最小异常安全的要求。

但是,如果Mutex包含一个没有设置任何属性的BinaryHeap,那么使用它的代码不太可能执行作者期望的行为。当然,程序也不可能正常运行下去。不过如果你能完全、绝对、百分之一百地肯定你可以用这些数据做点事情,Mutex还是提供了一个让你继续获得锁的方法。毕竟这是安全地,只不过可能没什么意义。