Rust push和pop

我们从push开始,实现一些真正的功能。它要做的事情就是检查空间是否已满,满了就扩容,然后写数据到下一个索引位置,最后增加长度。

写数据时,我们一定要小心,不要计算我们要写入的内存位置的值。最坏的情况,那块内存是一块未初始化的内存。最好的情况是那里存着我们已经pop出去的值。不管哪种情况,我们都不能直接索引这块内存然后解引用它,因为这样其实是把内存中的值当做了一个合法的T的实例。更糟糕的是,foo[idx] = x会调用foo[idx]处旧有值的drop方法!

正确的方法是使用ptr::write,它直接用值的二进制覆盖目标地址,不会计算任何的值。

对于push,如果原有的长度(调用push之前的长度)为0,那么我们就要写到第0个索引位置。所以我们应该用原有的长度做offset。

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

    unsafe {
        ptr::write(self.ptr.offset(self.len as isize), elem);
    }

    // 这一句不会失败,而会首先OOM
    self.len += 1;
}

pop是什么样的呢?尽管现在我们要访问的索引位置已经初始化了,Rust不允许我们用解引用的方式将值移出,因为那样的话整个内存都会回到未初始化状态!这时我们需要用ptr:read,它从目标位置拷贝出二进制值,然后解析成类型T的值。这时原有位置处的内存逻辑上是未初始化的,可实际上那里还是存在这一个正常的T的实例。

对于pop,如果原有长度是1,我们要读的是第0个索引位置。所以我们应该是按新的长度做offset。

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

我们应该实现Drop,否则就要造成大量的资源泄露了。最简单的方法是循环调用pop直到产生None为止,然后再回收我们的缓存。注意,当T: !Drop的时候,调用pop不是必须的。理论上我们可以问一问RustT是不是nee ...