不检查未初始化的内存(Unchecked Uninitialized Memory)
此规则的一个有趣例外是使用数组.Safe Rust不允许你部分初始化数组.初始化数组时,可以使用let x = [val; N]
设置相同的值,或者你可以使用let x = [val1, val2, val3]
单独指定每个成员.不幸的是,这非常严格,特别是如果你需要以更增量或动态的方式初始化数组.
Unsafe Rust为我们提供了一个强大的工具来处理这个问题:MaybeUninit
. 此类型可用于处理尚未完全初始化的内存.
使用MaybeUninit
,我们可以如下初始化一个数组的元素:
use std::mem::{self, MaybeUninit};
// Size of the array is hard-coded but easy to change (meaning, changing just
// the constant is sufficient). This means we can't use [a, b, c] syntax to
// initialize the array, though, as we would have to keep that in sync
// with `SIZE`!
const SIZE: usize = 10;
let x = {
// Create an uninitialized array of `MaybeUninit`. The `assume_init` is
// safe because the type we are claiming to have initialized here is a
// bunch of `MaybeUninit`s, which do not require initialization.
let mut x: [MaybeUninit<Box<u32>>; SIZE] = unsafe {
MaybeUninit::uninit().assume_init()
};
// Dropping a `MaybeUninit` does nothing. Thus using raw pointer
// assignment instead of `ptr::write` does not cause the old
// uninitialized value to be dropped.
// Exception safety is not a concern because Box can't panic
for i in 0..SIZE {
x[i] = MaybeUninit::new(Box::new(i as u32));
}
// Everything is initialized. Transmute the array to the
// initialized type.
unsafe { mem::transmute::<_, [Box<u32>; SIZE]>(x) }
};
dbg!(x);
此代码分三步进行:
创建一个
MaybeUninit<T>
数组. 对于当前稳定的Rust,我们必须为此使用不安全的代码:我们取一些未初始化的内存(MaybeUninit::uninit()
),并声称我们已经完全初始化了它(assume_init()) . 这似乎很荒谬,因为我们没有! 这是正确的原因是该数组本身完全由MaybeUninit
组成,而实际上并不需要初始化. 对于大多数其他类型,执行MaybeUninit::uninit().assume_init()
会产生该类型的无效实例,因此您会得到一些未定义的行为.初始化数组.这样做的微妙之处在于,通常,当我们使用
=
来赋值给一个Rust类型检查器认为已经初始化的值(例如x[i]
)时,存储在左手边的旧值会被删除. 这将是一场灾难. 但是,在本例中,左侧的类型是MaybeUninit<Box<u32>>
,删除不会做任何事情! 有关此drop
问题的更多讨论,请参见下文.最后,我们必须更改数组的类型以删除
MaybeUninit
. 对于当前稳定的Rust,这需要一个transmute
. 这种转换是合法的,因为在内存中,MaybeUninit<T>
看起来与T
相同. 但是,请注意,通常,Container<MaybeUninit<T>>>
确实与Container<T>
看起来 不(not) 一样! 想象一下,如果Container
是Option
,而T
是bool
,那么Option<bool>
会利用bool
只有两个有效值,但是Option<MaybeUninit<bool>>
不能做到这一点. 因为bool
不必初始化. 因此,它取决于Container
是否允许转换MaybeUninit
. 对于数组,它是(最终标准库将通过提供适当的方法来确认这一点).
在中间的循环上花费更多的时间是值得的,尤其是赋值运算符及其与drop
的交互.如果我们这样写
*x[i].as_mut_ptr() = Box::new(i as u32); // WRONG!
我们实际上会覆盖Box<u32>
,从而导致未初始化数据的drop
,这会引起很多悲伤和痛苦.
如果由于某种原因我们不能使用MaybeUninit::new
,则正确的替代选择是使用ptr
模块. 特别是,它提供了三个函数,这些函数使我们能够在不删除旧值的情况下将字节分配到内存中的某个位置:write
,copy
和copy_nonoverlapping
.
ptr::write(ptr, val)
接受一个val
并将其移动到ptr
指向的地址.ptr::copy(src, dest, count)
将计数count
T将占用的位从src复制到dest.(这相当于memmove—注意参数顺序是相反的!)ptr::copy_nonoverlapping(src, dest, count)
执行copy
操作,但假设两个内存范围不重叠,则会快一点.(这相当于memcpy—注意参数顺序是相反的!)
不言而喻,这些函数如果被滥用,将会造成严重破坏,或者直接导致未定义行为.这些函数 本身(themselves) 唯一需要的是分配你想要读写的位置,并正确对齐它们.然而,将任意位写入内存的任意位置的方式可能会破坏事物基本上是不可数的!
值得注意的是,你不需要担心ptr::write
风格的诡计的类型没有实现Drop
或包含Drop
类型,因为Rust知道不会尝试删除它们.这就是我们在上面的示例中所依赖的.
但是,在使用未初始化的内存时,你需要始终保持警惕,警惕Rust在完全初始化之前尝试删除你制作的值.通过该变量作用域的每个控制路径必须在结束之前初始化该值(如果它有析构函数).这包括代码恐慌.MaybeUninit
在这里有所帮助,因为它不会隐式删除其内容—但所有这些真正的意思是在panic的情况下,您将导致已经初始化的部分的内存泄漏,而不是未初始化部分的双重释放.
注意,要使用ptr
方法,你首先需要获得一个指向要初始化的数据的 原始指针(raw pointer) . 构造对未初始化数据的 引用(reference) 是非法的,这意味着在获取所述原始指针时必须要小心:
- 对于
T
的数组,可以使用base_ptr.add(idx)
,其中base_ptr: *mut T
来计算数组索引idx
的地址. 这取决于数组在内存中的布局方式. - 但是,对于一个结构,通常我们不知道它的布局方式,并且我们也不能使用
&mut base_ptr.field
,因为那样会创建一个引用. 因此,当前无法创建指向部分初始化的结构的字段的原始指针,也无法初始化部分初始化的结构的单个字段.(解决这个问题的方案正在研究中.)
最后一点:阅读旧的Rust代码时,您可能会发现不推荐使用的mem::uninitialized
函数. 该函数曾经是处理栈上未初始化内存的唯一方法,但事实证明,无法与该语言的其余部分正确集成. 始终在新代码中使用MaybeUninit
,并在有机会时移植旧代码.
这就是使用未初始化的内存!基本上没有任何地方需要未初始化的内存,所以如果你要传递它,一定要 非常(really) 小心.