Rust高级代码题 - 实现一个 MiniVec<T>

0 阅读3分钟

Hi guys, it's Kevin here

作为一个资深Rust开发,现在想做一个Coding挑战系列,希望大家能从里面加深对ownership/lifetime, smart pointer, async, concurrency的理解。好的,直接进入正题

手写实现一个MiniVec

题目:

不依赖standard lib的Vec, 自己封装一个简化版的MiniVec, 支持:

  • new()
  • push(value: T)
  • pop() -> Option
  • get(index: usize) -> Option<&T>
  • get_mut(index: usize) -> Option<&T>
  • len() -> usize

要求:

  • 底层用堆内存管理
  • 能自动扩容
  • 需要正确处理析构,避免内存泄漏
  • 不允许直接拿 Vec<T> 当底层存储

考察点:

  • std::alloc

  • 裸指针

  • MaybeUninit

  • 所有权与析构

  • Drop

解题思路:

第一步:明确需要哪些底层工具

实现动态数组需要手动控制堆内存,Rust 提供了三个核心工具:

  • std::alloc::alloc / realloc / dealloc 负责原始内存的申请、扩容和释放;
  • std::ptr::write / read 负责在不触发 drop 的前提下向内存槽写入或读出值;
  • std::alloc::Layout 用来描述"我需要多大、对齐多少字节的内存"。

第二步:设计字段

一个动态数组在运行时需要记录三件事:

堆上的首地址  ptr: NonNull<T>
当前容量      cap: usize
存活元素数    len: usize

另外加一个零大小的 PhantomData<T>,目的不是存数据,而是告诉编译器"我逻辑上拥有 T",让 drop check 和协变检查正常工作。

第三步:逐个击破五个接口

new() 最简单——不分配任何内存,用 NonNull::dangling() 作为合法的初始指针(cap=0 时永远不会解引用它),一切都是零开销。

push() 分两步:先判断是否已满(len == cap),满了就调内部的 grow();然后用 ptr::write 把值写到 ptr + len 的位置,最后 len += 1。关键点是必须用 ptr::write 而不是赋值,因为那块内存是未初始化的,赋值会先 drop 旧值,而 drop 未初始化内存是 UB。

grow() 是整个实现中最复杂的部分。逻辑是:首次分配给 4 个槽,之后每次翻倍。首次用 alloc,后续用 realloc(让操作系统有机会原地扩展,避免不必要的拷贝)。分配失败调 handle_alloc_error 直接 abort,与标准库行为保持一致。

pop()push 对称:先 len -= 1,再用 ptr::read 把值"移走"。ptr::read 按位复制并返回值,所有权转移给调用者,该槽在逻辑上已不存在,不会被二次 drop。

get / get_mut 核心是边界检查 i >= self.len(注意是 len 不是 cap,cap 之外的槽未初始化)。通过检查后,裸指针加偏移再转引用即可。

第四步:实现 Drop(最容易踩坑的地方)

Drop 必须做两件事,顺序不能颠倒:

① 逐一 drop 所有存活元素   ← 释放元素自己持有的资源(如 String 的堆)
② dealloc 整块缓冲区       ← 释放 MiniVec 自身的堆内存

如果先 dealloc 再 drop 元素,元素 drop 时访问的内存已经归还给分配器,就是 use-after-free。最简单的实现是复用 pop()——它本身就会正确地把元素移走并让其离开作用域自动 drop。

第五步:处理两个容易忘记的细节

Send / Sync 标记:使用裸指针之后编译器默认把类型标记为 !Send + !Sync。但 MiniVec<T> 独占数据,只要 T: Send 就可以安全跨线程发送,只要 T: Sync 就可以安全共享引用,需要手动加 unsafe impl

ZST 处理:零大小类型(如 ())的 size_of::<T>() == 0Layout::array 计算出的 size 也是 0,alloc(0) 的行为是实现定义的。最简单的做法是在 grow() 开头直接 assert! 禁止 ZST,或者走完全独立的 ZST 路径(标准库的做法是将容量逻辑设为 usize::MAX,永远不触发分配)。

Struct Implementation

pub struct MiniVec<T> {
   ptr:     NonNull<T>,    // 堆上数据的首地址(非空保证)
   cap:     usize,          // 已分配的槽位数
   len:     usize,          // 当前存活的元素数
   _marker: PhantomData<T>, // 向编译器声明"拥有 T 的数据"
}

Complete Implementation:

github.com/KevinSheera…