非安全方式

一个特殊情况是数组。安全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处将count个T占用的字节拷贝到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中你会有很多和它打交道的机会,特别是在使用标准库的时候。这个模式简单来说 ...