压入和弹出

好了。我们可以初始化。我们可以分配。让我们真正实现一些功能!让我们从 push 开始。它所需要做的就是检查我们是否已满需要增长,无条件地写入下一个索引,然后增加我们的长度。

在进行写入时,我们必须小心不要评估我们要写入的内存。最坏的情况是,它是来自分配器的真正未初始化的内存。最好的情况是,它是我们弹出的某个旧值的位。无论哪种方式,我们都不能仅仅索引到内存并解引用它,因为这会将内存评估为 T 的有效实例。更糟糕的是,foo[idx] = x 会尝试在 foo[idx] 的旧值上调用 drop

正确的做法是使用 ptr::write,它只是用我们提供的值的位盲目地覆盖目标地址。不涉及评估。

对于 push,如果旧的 len(在调用 push 之前)为 0,那么我们想写入第 0 个索引。所以我们应该偏移旧的 len。

pub fn push(&mut self, elem: T) {
    if self.len == self.cap { self.grow(); }

    unsafe {
        ptr::write(self.ptr.as_ptr().add(self.len), elem);
    }

    // Can't fail, we'll OOM first.
    self.len += 1;
}

简单!那 pop 呢?虽然这次我们要访问的索引已初始化,但 Rust 不会让我们直接解引用内存位置来移出值,因为这会使内存未初始化!为此,我们需要 ptr::read,它只是从目标地址复制出位并将其解释为类型 T 的值。这将使该地址处的内存逻辑上未初始化,即使实际上那里有一个完全良好的 T 实例。

对于 pop,例如,如果旧的 len 为 1,我们想从第 0 个索引读取。所以我们应该偏移新的 len。

pub fn pop(&mut self) -> Option<T> {
    if self.len == 0 {
        None
    } else {
        self.len -= 1;
        unsafe {
            Some(ptr::read(self.ptr.as_ptr().add(self.len)))
        }
    }
}