主题
Rust 深入理解 Vec(动态数组)与所有权模型
如果说 String 是 Rust 中处理文本的主力,那么 Vec<T>(Vector,向量)就是处理 列表数据 的绝对核心。
本文将深入解析 Vec 的工作原理,通过图解和实例,带你彻底理解它是如何与 Rust 的 所有权(Ownership) 和 借用(Borrowing) 规则互动的。
一、Vec 的内存布局:它到底长什么样?
在深入代码之前,我们必须先理解 Vec 在内存中是什么样子的。这能帮你瞬间理解很多奇怪的报错。
Vec<T> 其实由两部分组成:
- 栈(Stack)上的胖指针(Fat Pointer):包含三个字段。
ptr:指向堆上数据的指针。len:当前有多少个元素。capacity:堆上分配了多少空间(容量)。
- 堆(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 会随着元素的增加不断进行“搬家”:
- 找一块更大的新内存。
- 把旧数据拷贝过去。
- 释放旧内存。 这非常消耗性能!
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) |
八、总结
- 内存模型:
Vec是“栈上指针” + “堆上数据”。 - 所有权:
Vec拥有其元素。Vec死了,元素陪葬。 - 迭代器:分清
iter()(借阅)、iter_mut()(批注)、into_iter()(拿走)。 - 性能:预知大小时用
with_capacity,删除无序数据时用swap_remove。
理解了 Vec,你就理解了 Rust 内存管理的半壁江山。