Skip to content

Rust 深入理解 Vec(动态数组)与所有权模型

如果说 String 是 Rust 中处理文本的主力,那么 Vec<T>(Vector,向量)就是处理 列表数据 的绝对核心。

本文将深入解析 Vec 的工作原理,通过图解和实例,带你彻底理解它是如何与 Rust 的 所有权(Ownership)借用(Borrowing) 规则互动的。


一、Vec 的内存布局:它到底长什么样?

在深入代码之前,我们必须先理解 Vec 在内存中是什么样子的。这能帮你瞬间理解很多奇怪的报错。

Vec<T> 其实由两部分组成:

  1. 栈(Stack)上的胖指针(Fat Pointer):包含三个字段。
    • ptr:指向堆上数据的指针。
    • len:当前有多少个元素。
    • capacity:堆上分配了多少空间(容量)。
  2. 堆(Heap)上的数据:实际存放元素的地方。
text
       栈 (Stack)             堆 (Heap)
    +--------------+       +---+---+---+---+---+
    | ptr      | ●-------> | 1 | 2 | 3 | ? | ? |
    | len      | 3 |       +---+---+---+---+---+
    | capacity | 5 |         0   1   2   3   4
    +--------------+
  • len = 3:说明现在只有 [1, 2, 3] 这三个有效元素。
  • capacity = 5:说明我们申请了 5 个位置,还可以再放 2 个元素而不需要重新分配内存。

关键点:当你把 Vec 赋值给另一个变量时,拷贝的是 栈上的那三个字段(ptr, len, cap),堆上的数据 不会被拷贝。这就是所有权转移(Move)的物理基础。


二、创建 Vec:性能优化的第一步

1. 基础创建

最常见的方式:

rust
// 1. 使用 new(),此时没有分配堆内存
let mut v: Vec<String> = Vec::new();

// 2. 使用宏,直接初始化
let v2 = vec![1, 2, 3];

2. 预分配内存(高级技巧)

如果你知道大概要存多少数据,一定要用 Vec::with_capacity

rust
// 预先在堆上分配好 10 个位置
let mut v = Vec::with_capacity(10);

为什么要这么做? 如果不预分配,Vec 会随着元素的增加不断进行“搬家”:

  1. 找一块更大的新内存。
  2. 把旧数据拷贝过去。
  3. 释放旧内存。 这非常消耗性能!with_capacity 可以避免这种无谓的搬家。

三、所有权与 Move:把元素放进去

当我们往 Vec 里放东西时,所有权发生了什么变化?

rust
let mut v = Vec::new();
let s = String::from("Hello");

// s 的所有权被“移动”进了 v
v.push(s);

// println!("{}", s); // ❌ 错误!s 已经失效了,它现在归 v 所有

解析Vec 就像一个保险箱。当你把 s 放进去(push)之后,s 就不再属于你了,它属于这个保险箱。保险箱销毁时,里面的东西也会一起销毁。


四、读取元素:要在“方便”与“安全”中做选择

读取 Vec 中的元素,Rust 提供了两种截然不同的方式。

1. 索引访问 &v[index] —— 方便但危险

rust
let v = vec![10, 20, 30];
let n = &v[100]; // ❌ 程序直接崩溃(Panic)!
  • 像 C/C++ 数组:直接访问内存偏移。
  • 风险:如果你算错了索引,程序会直接挂掉,防止读取非法内存。

2. 安全访问 .get(index) —— 推荐做法

rust
let v = vec![10, 20, 30];

match v.get(100) {
    Some(val) => println!("找到了:{}", val),
    None => println!("越界了,但这很安全!"),
}
  • 返回 Option:它不会崩溃,而是返回 None
  • 适用场景:任何你不确定索引是否合法的场景(比如用户输入)。

五、迭代:三种姿势,三种含义(核心难点)

这是新手最容易晕的地方:iter, iter_mut, into_iter 到底有什么区别? 我们可以用 “读书” 来打比方。

1. iter() —— 借书来看(只读)

类型&T(不可变引用)

rust
let v = vec![String::from("A"), String::from("B")];

for s in v.iter() {
    // s 的类型是 &String
    println!("我正在读:{}", s);
    // s.push_str("Change"); // ❌ 错误!借来的书不能乱涂乱画
}

// 循环结束后,v 还在,书还是你的
println!("书架还在:{:?}", v);

2. iter_mut() —— 借书来做笔记(可修改)

类型&mut T(可变引用)

rust
let mut v = vec![String::from("A"), String::from("B")];

for s in v.iter_mut() {
    // s 的类型是 &mut String
    s.push_str("_modified"); // ✅ 可以修改
}

// 循环结束后,v 还在,但内容变了
println!("书被修改了:{:?}", v);

3. into_iter() —— 把书送人(所有权转移)

类型T(所有权)

rust
let v = vec![String::from("A"), String::from("B")];

for s in v.into_iter() {
    // s 的类型是 String(真身)
    // 你可以把书卖了、烧了、送给别人
    let backup = s;
}

// println!("{:?}", v); // ❌ 错误!书架(Vec)已经被拆了,书也没了

记忆口诀

  • iter():借来看(&)
  • iter_mut():借来改(&mut)
  • into_iter():拿走不还(Move)

六、除了 get 和 remove,还有哪些“隐藏神器”?

Vec 的 API 极其丰富,掌握这些方法能让你的代码少写很多循环。

1. 高效删除:swap_remove

如果你想删除数组中间的一个元素,用 remove 会很慢,因为它要把后面的所有元素往前挪(填补空缺)。 如果你 不在乎顺序,用 swap_remove

rust
let mut v = vec!["A", "B", "C", "D"];
// 把最后一个元素 "D" 搬过来覆盖 "B",然后把原来的 "B" 删掉
let x = v.swap_remove(1);

println!("删掉了:{}", x); // "B"
println!("现在的 Vec:{:?}", v); // ["A", "D", "C"] -> 顺序变了,但速度极快 O(1)

2. 批量筛选:retain

不要再写 for 循环加 if 来删除了,用 retain 一行搞定。

rust
let mut v = vec![1, 2, 3, 4, 5, 6];
// 只保留偶数
v.retain(|&x| x % 2 == 0);

println!("{:?}", v); // [2, 4, 6]

3. 切片操作:drain

如果你想把 Vec 的一部分“割”下来给别人,或者放到另一个 Vec 里。

rust
let mut v = vec![1, 2, 3, 4, 5];
// 把索引 1 到 3(不包含 4)的元素割下来
let sub: Vec<_> = v.drain(1..4).collect();

println!("原来的 v 只剩:{:?}", v); // [1, 5]
println!("割下来的部分:{:?}", sub); // [2, 3, 4]

4. 去重:dedup

注意:它只能去除 连续 的重复元素。所以通常先排序,再去重。

rust
let mut v = vec![1, 2, 2, 3, 2];
v.sort(); // 先排序变成 1, 2, 2, 2, 3
v.dedup(); // 去重
println!("{:?}", v); // [1, 2, 3]

5. 拆分:split_off

把一个 Vec 一刀两断。

rust
let mut v = vec![1, 2, 3, 4, 5];
// 从索引 3 开始切断
let v2 = v.split_off(3);

println!("前半段:{:?}", v);  // [1, 2, 3]
println!("后半段:{:?}", v2); // [4, 5]

七、常见方法速查表

分类方法说明复杂度
push(v)末尾追加O(1)
insert(i, v)中间插入(慢,尽量少用)O(n)
append(&mut other)把另一个 Vec 接过来(Move)O(m)
pop()弹出末尾元素O(1)
remove(i)删除指定位置(慢,需移位)O(n)
swap_remove(i)删除指定位置(快,不保序)O(1)
clear()清空所有O(n)
truncate(len)截断到指定长度O(n)
contains(&v)是否包含O(n)
binary_search(&v)二分查找(需有序)O(log n)
reserve(n)预分配内存O(n)
shrink_to_fit()释放多余内存O(n)

八、总结

  1. 内存模型Vec 是“栈上指针” + “堆上数据”。
  2. 所有权Vec 拥有其元素。Vec 死了,元素陪葬。
  3. 迭代器:分清 iter()(借阅)、iter_mut()(批注)、into_iter()(拿走)。
  4. 性能:预知大小时用 with_capacity,删除无序数据时用 swap_remove

理解了 Vec,你就理解了 Rust 内存管理的半壁江山。