前言
上周我在写代码的时候突然好奇:Rust 的 Vec 到底是怎么实现的?为什么它能这么快?于是我打开了标准库的源码,这一看不要紧,发现里面有好多有意思的细节。
今天就来分享一下我的"考古"成果。不会讲得太学术(主要是我也不是科班出身),就当是聊天,看看 Rust 标准库里那些聪明的设计。
Vec 到底长什么样?
打开 vec.rs,第一眼看到的是这个:
rust
pub struct Vec<T> {
buf: RawVec<T>,
len: usize,
}
嗯?就这?我以为会有什么魔法,结果就两个字段。
再看看 RawVec 是啥:
rust
pub struct RawVec<T> {
ptr: Unique<T>,
cap: usize,
}
好了,现在清楚了。一个 Vec<T> 其实就是:
-
ptr:指向堆上数据的指针
-
cap:容量(capacity),分配了多少空间
-
len:长度(length),实际存了多少元素

有意思的点 :len 永远 ≤ cap。如果 len == cap,下次 push 的时候就得扩容了。
扩容策略:没你想的那么简单
我一开始以为 Vec 扩容就是简单的"满了就分配 2 倍空间"。看了源码才发现,还真不是这样。
rust
fn grow_amortized(&mut self, len: usize, additional: usize) -> Result<(), TryReserveError> {
let required_cap = len.checked_add(additional).ok_or(...)?;
// 如果当前容量够用,直接返回if self.cap >= required_cap {
return Ok(());
}
// 计算新容量let cap = cmp::max(self.cap * 2, required_cap);
let cap = cmp::max(Self::MIN_NON_ZERO_CAP, cap);
// ...
}
关键就在这一行:
let cap = cmp::max(self.cap * 2, required_cap);
翻译一下:新容量是"当前容量的 2 倍"和"需要的容量"中的较大值。
为什么要这样?
我写了个小程序测试了一下:
rust
fn main() {
let mut v = Vec::new();
for i in 0..10 {
v.push(i);
println!("len: {}, cap: {}", v.len(), v.capacity());
}
}
输出:
len: 1, cap: 4
len: 2, cap: 4
len: 3, cap: 4
len: 4, cap: 4
len: 5, cap: 8
len: 6, cap: 8
len: 7, cap: 8
len: 8, cap: 8
len: 9, cap: 16
len: 10, cap: 16
看到了吗?
-
第一次 push 时,容量直接变成 4(这是
MIN_NON_ZERO_CAP) -
之后每次扩容都是翻倍:4 → 8 → 16 → ...
为什么不每次只增加 1?
假设你要往 Vec 里 push 1000 个元素:
-
每次增加 1:需要扩容 1000 次,每次都要拷贝所有旧数据
-
翻倍策略:只需要扩容 10 次左右(2^10 = 1024)
这就是所谓的"摊还复杂度"(amortized complexity)。虽然单次扩容很贵,但平均下来每个元素的插入成本是 O(1)。

那个神秘的 Unique<T> 是干嘛的?
我刚开始看到 Unique<T> 的时候,心想:"为啥不直接用 *mut T?"
看了注释才恍然大悟:
rust
pub struct Unique<T: ?Sized> {
pointer: *const T,
_marker: PhantomData<T>,
}
几个关键点:
-
它是
*const T而不是*mut T:这样编译器不会因为"可变引用"的限制影响优化 -
但它代表唯一所有权:告诉编译器"只有我持有这个指针,你可以放心优化"
-
PhantomData 很重要:它让编译器知道这个类型"拥有" T,这影响 Drop 检查和类型协变
举个例子,如果用普通的 *mut T:
// 编译器可能会这样想:
// "这个指针可能被多个地方共享,我得保守点"
但用了 Unique<T> 后:
// 编译器:"哦,这是独占的,我可以激进优化了"
结果就是更好的性能,特别是在涉及别名分析(alias analysis)的时候。
Drop 的小心思
Vec 的 Drop 实现让我看了好几遍才理解:
rust
unsafe impl<#[may_dangle] T> Drop for Vec<T> {
fn drop(&mut self) {
unsafe {
// 先 drop 所有元素
ptr::drop_in_place(ptr::slice_from_raw_parts_mut(
self.as_mut_ptr(),
self.len
))
}
// RawVec 的 drop 会释放内存
}
}
看到那个 #[may_dangle] 了吗?这是个高级特性。
简单来说:它告诉编译器"我保证在 drop Vec 的时候不会访问 T 的内容(除了 drop 它们)"。
为什么需要这个?
看这个例子:
rust
struct Foo<'a>(&'a str);
fn main() {
let s = String::from("hello");
let mut v = Vec::new();
v.push(Foo(&s));
drop(s); // s 被 drop 了drop(v); // v 还持有 Foo,而 Foo 里有 &s
}
如果没有 #[may_dangle],这段代码会编译失败,因为编译器担心 Vec 的 drop 可能访问已经无效的引用。
但 Vec 的实现保证了:drop 的时候只会 drop T,不会读取 T 的内容。所以加了 #[may_dangle] 后,这段代码就能编译了。
我的理解:这是 Rust 在"严格的安全性"和"实际的可用性"之间找到的平衡点。
push 和 pop:看似简单,实则精妙
push 的实现
rust
pub fn push(&mut self, value: T) {
if self.len == self.buf.capacity() {
self.buf.grow_amortized(self.len, 1);
}
unsafe {
let end = self.as_mut_ptr().add(self.len);
ptr::write(end, value);
self.len += 1;
}
}
逐行解析:
-
检查容量够不够,不够就扩容
-
计算新元素的位置:
ptr + len -
关键 :用
ptr::write而不是赋值
为什么用 ptr::write?
如果用普通赋值,编译器会先尝试 drop 目标位置的旧值。但这个位置根本没有旧值(是未初始化内存)!ptr::write 就是为这种情况设计的------直接写入,不 drop。
pop 的对称设计
rust
pub fn pop(&mut self) -> Option<T> {
if self.len == 0 {
None
} else {
unsafe {
self.len -= 1;
Some(ptr::read(self.as_ptr().add(self.len)))
}
}
}
注意:
-
先减少 len :这样即使
ptr::readpanic 了,Vec 的状态也是正确的 -
用 ptr::read:它会拷贝值出来,但不 drop 原位置的值(因为那块内存已经"不属于" Vec 了)
这种对称设计很优雅:
-
push:ptr::write+len++ -
pop:len--+ptr::read

迭代器的魔法
Vec 的迭代器实现也很有意思。看这个:
rust
pub struct IntoIter<T> {
buf: NonNull<T>,
cap: usize,
ptr: *const T, // 当前读取位置
end: *const T, // 结束位置
}
思路 :用两个指针标记范围,每次 next() 就移动 ptr,每次 next_back() 就移动 end。
rust
impl<T> Iterator for IntoIter<T> {
type Item = T;
fn next(&mut self) -> Option<T> {
if self.ptr == self.end {
None
} else {
unsafe {
let old = self.ptr;
self.ptr = self.ptr.offset(1);
Some(ptr::read(old))
}
}
}
}
巧妙之处:
-
这样实现的迭代器是
DoubleEndedIterator(可以从两端迭代) -
而且是
ExactSizeIterator(可以精确知道剩余元素数量) -
内存管理也很简单:Drop 的时候释放
[ptr, end)之间的元素
试试这个:
rust
fn main() {
let v = vec![1, 2, 3, 4, 5];
let mut iter = v.into_iter();
println!("{:?}", iter.next()); // Some(1)println!("{:?}", iter.next_back()); // Some(5)println!("{:?}", iter.next()); // Some(2)println!("{:?}", iter.next_back()); // Some(4)println!("{:?}", iter.next()); // Some(3)println!("{:?}", iter.next()); // None
}
指针在内存中这样移动:

一些让我吃惊的优化
- 零大小类型(ZST)的特殊处理
rust
impl<T> Vec<T> {
fn is_zst() -> bool {
mem::size_of::<T>() == 0
}
}
如果 T 是零大小类型(比如 ()),Vec 根本不分配内存!
rust
fn main() {
let mut v: Vec<()> = Vec::new();
for _ in 0..1000000 {
v.push(());
}
println!("cap: {}", v.capacity()); // 输出很大的数字// 但实际上一个字节都没分配!
}
原因?反正每个 () 都占 0 字节,分配内存纯属浪费。Vec 只需要维护 len 就行了。
- append 的优化
rust
pub fn append(&mut self, other: &mut Vec<T>) {
unsafe {
self.append_elements(other.as_slice() as _);
other.set_len(0);
}
}
append 不是一个个 push,而是:
-
一次性扩容到足够大小
-
批量拷贝内存(用
ptr::copy_nonoverlapping) -
把另一个 Vec 的 len 设为 0(这样 drop 的时候不会重复释放)
比循环 push 快多了!
- 内联优化
看这个注释:
#[inline]pub fn push(&mut self, value: T) {
// ...
}
push、pop、len 这些方法都标记了 #[inline]。
效果:在 release 模式下,编译器会把这些函数调用内联到调用点,消除函数调用开销。
我测试了一下:
rust
// 没优化的版本fn sum_no_inline(v: &Vec<i32>) -> i32 {
let mut sum = 0;
for i in 0..v.len() { // len() 每次都是函数调用
sum += v[i];
}
sum
}
// 优化的版本fn sum_with_inline(v: &Vec<i32>) -> i32 {
v.iter().sum() // 迭代器 + sum,全部内联
}
在 release 模式下,第二个版本快了大约 20%。
我学到的几个教训
- 不要小看标准库
以前我觉得 Vec 就是个"动态数组",有啥复杂的?看了源码才知道,里面有太多细节:
-
扩容策略
-
内存安全保证
-
性能优化
-
边界情况处理
每一个细节都经过深思熟虑。
- unsafe 不可怕
Vec 的实现里到处都是 unsafe,但这不代表它不安全。
关键是 :unsafe 块把"需要人工检查的部分"和"编译器能检查的部分"分开了。只要 unsafe 块的代码是正确的,整个 Vec 就是安全的。
这也是 Rust 的哲学:把不安全的部分集中在少数几个地方,仔细审查它们,然后在上面构建安全的抽象。
- 性能优化无处不在
从内联优化到 ZST 特殊处理,从扩容策略到批量拷贝,Vec 的每个角落都在为性能考虑。
但同时,可读性和正确性从未被牺牲。代码写得很清楚,该加的断言都加了,该处理的边界情况都处理了。
推荐的阅读路径
如果你也想读标准库源码,我的建议:
-
从 Vec 开始:它足够简单,但也足够有代表性
-
配合文档看:标准库的文档写得很好,解释了很多设计决策
-
动手实验:写点测试代码,验证你的理解
-
不要急:一次搞懂一小块就行,慢慢积累
调试小技巧:
rust
fn main() {
let mut v = Vec::new();
// 用这个看内部状态println!("ptr: {:p}, cap: {}, len: {}",
v.as_ptr(), v.capacity(), v.len());
v.push(1);
println!("ptr: {:p}, cap: {}, len: {}",
v.as_ptr(), v.capacity(), v.len());
}
输出:
ptr: 0x1, cap: 0, len: 0 // 空 Vec,指针是 dangling
ptr: 0x7f8a3c000000, cap: 4, len: 1 // push 后分配了内存
总结
读 Vec 源码这一下午,我最大的收获是:
-
简单的接口背后可能有复杂的实现:Vec 用起来很简单,但实现起来要考虑很多东西
-
性能和安全可以兼得:Vec 既快又安全,证明了 Rust 的设计是成功的
-
读源码是学习的好方法:比看文档更深入,比自己摸索更高效