学习资源
-
Rust 程序设计语言(2024 edition)简体中文版 GitHub 仓库:https://github.com/KaiserY/trpl-zh-cn
-
B站视频教程 【Rust编程语言入门教程(Rust语言/Rust权威指南配套)【已完结】】 https://www.bilibili.com/video/BV1hp4y1k7SV/?share_source=copy_web&vd_source=28d07063ae73341866c4483f21f5f907
视频和 GitHub 的资料是配套的
Rust Vector 动态数组详解
核心概念:
Vec<T>是 Rust 标准库提供的动态数组类型,允许在单个数据结构中存储多个相同类型的值,这些值在内存中连续排列。
一、Vector 简介
什么是 Vector?
Vec<T>(也被称为 vector)是 Rust 中最常用的集合类型之一:
- 动态大小:可以在运行时增长或缩小
- 连续存储:所有元素在内存中彼此相邻排列
- 类型统一:只能存储相同类型的值
- 堆分配:数据存储在堆上,可以动态调整大小
使用场景
Vector 在以下场景下非常实用:
- 文件中的文本行
- 购物车中商品的价格
- 需要动态增长的列表
- 任何需要存储一系列相同类型值的场景
Vector 的底层实现
核心原理:Vec 的底层就是堆上的动态数组。
栈上的 Vec 结构体 堆上的数组
┌─────────────────┐ ┌───┬───┬───┬───┬───┬───┐
│ ptr: *mut T │───────────────>│ 1 │ 2 │ 3 │ 4 │ │ │
│ len: usize │ = 4 └───┴───┴───┴───┴───┴───┘
│ capacity: usize │ = 6 已使用 未使用
└─────────────────┘ (len=4) (cap-len=2)
Vec 的三个核心字段:
ptr(指针):指向堆上数组的起始地址len(长度):当前已存储的元素数量capacity(容量):堆上数组的总容量(已分配的空间)
关键特性:
- Vec 本身是一个小的结构体,存储在栈上(只包含 3 个字段)
- 实际的数据存储在堆上的连续内存块中
- 当容量不足时,会重新分配更大的堆内存,并拷贝旧数据
Vector vs 数组的内存布局
// 栈上的数组:所有数据都在栈上
let arr: [i32; 3] = [1, 2, 3];
// 内存布局:
// 栈: [1, 2, 3] <- 所有数据都在这里
// 堆上的 Vector:数据在堆上
let vec: Vec<i32> = vec![1, 2, 3];
// 内存布局:
// 栈: [ptr, len=3, cap=3] <- Vec 结构体
// |
// v
// 堆: [1, 2, 3] <- 实际数据
为什么 Vec 使用堆?
- 动态大小:栈上的数组大小必须在编译时确定,而 Vec 可以在运行时动态增长
- 大数据:堆可以存储更大的数据,栈空间有限(通常只有几 MB)
- 所有权转移:Vec 的所有权转移只需要拷贝栈上的 3 个字段,而不需要拷贝堆上的所有数据
二、创建 Vector
方法 1:使用 Vec::new() 创建空 Vector
let v: Vec<i32> = Vec::new();
说明:
- 这里需要类型注解
Vec<i32> - 因为没有插入任何值,Rust 无法推断要存储的元素类型
Vec<T>是泛型类型,T表示可以存放任何类型
关键点:
Vec是标准库提供的类型- 当
Vec存放某个特定类型时,该类型位于尖括号<>中 - 例如:
Vec<i32>表示存放i32类型元素的 vector
方法 2:使用 vec! 宏创建带初始值的 Vector(推荐)
let v = vec![1, 2, 3];
说明:
vec!宏会根据提供的值自动创建Vec- Rust 可以推断出类型为
Vec<i32>,不需要类型注解 - 这是更常见、更方便的做法
创建方式对比
| 方式 | 语法 | 是否需要类型注解 | 使用场景 |
|---|---|---|---|
Vec::new() |
let v: Vec<i32> = Vec::new(); |
✅ 需要 | 创建空 vector,稍后添加元素 |
vec! 宏 |
let v = vec![1, 2, 3]; |
❌ 不需要 | 创建带初始值的 vector(推荐) |
三、更新 Vector
使用 push 方法添加元素
let mut v = Vec::new();
v.push(5);
v.push(6);
v.push(7);
v.push(8);
关键点:
- 必须使用
mut关键字使 vector 可变 - Rust 根据插入的数据推断类型为
Vec<i32> - 不需要显式的
Vec<i32>类型注解
Vector 的扩容机制
当 push 元素时,如果容量不足,Vec 会自动扩容:
let mut v = Vec::new();
println!("初始: len={}, cap={}", v.len(), v.capacity()); // len=0, cap=0
v.push(1);
println!("push 1: len={}, cap={}", v.len(), v.capacity()); // len=1, cap=4
v.push(2);
v.push(3);
v.push(4);
println!("push 4: len={}, cap={}", v.len(), v.capacity()); // len=4, cap=4
v.push(5); // 触发扩容!
println!("push 5: len={}, cap={}", v.len(), v.capacity()); // len=5, cap=8
Vec 的初始容量:
Vec::new()创建的 Vec 初始容量为 0(不分配堆内存)- 第一次
push时会分配初始容量,通常是 4(具体值取决于元素类型大小) - 之后采用倍增策略(capacity 翻倍)
扩容过程:
1. 初始状态(cap=0)
栈: [ptr=null, len=0, cap=0]
堆: 无分配
2. push(1) - 第一次 push,分配初始容量(通常是 4)
栈: [ptr, len=1, cap=4]
|
v
堆: [1, _, _, _]
3. push(2), push(3), push(4) - 容量足够,直接添加
栈: [ptr, len=4, cap=4]
|
v
堆: [1, 2, 3, 4]
4. push(5) - 容量不足,触发扩容(翻倍:4 → 8)
a. 分配新的堆内存(cap=8)
b. 拷贝旧数据到新内存
c. 释放旧内存
d. 更新 ptr 和 cap
栈: [ptr_new, len=5, cap=8]
|
v
堆: [1, 2, 3, 4, 5, _, _, _]
5. 继续 push - 容量足够时直接添加,不足时再次翻倍(8 → 16 → 32 ...)
扩容策略详解:
- 初始容量:0(延迟分配,节省内存)
- 首次分配:通常是 4(或根据元素大小调整)
- 后续扩容:采用倍增策略(capacity 翻倍)
- 优势:保证
push操作的平摊时间复杂度为 O(1) - 原因:避免频繁的内存分配和拷贝
实际测试:
let mut v: Vec<i32> = Vec::new();
println!("new(): cap={}", v.capacity()); // 0
v.push(1);
println!("push 1: cap={}", v.capacity()); // 4
v.push(2); v.push(3); v.push(4);
println!("push 2-4: cap={}", v.capacity()); // 4
v.push(5);
println!("push 5: cap={}", v.capacity()); // 8
// 继续 push 到触发下一次扩容
for i in 6..=8 {
v.push(i);
}
println!("push 6-8: cap={}", v.capacity()); // 8
v.push(9);
println!("push 9: cap={}", v.capacity()); // 16
性能考虑:
- 如果预知最终大小,可以使用
Vec::with_capacity(n)预分配容量 - 避免不必要的扩容和内存拷贝
// 方式 1:Vec::new() - 初始容量为 0
let mut v1 = Vec::new();
println!("Vec::new(): cap={}", v1.capacity()); // 0
// 方式 2:vec! 宏 - 根据元素数量分配精确容量
let v2 = vec![1, 2, 3];
println!("vec![1, 2, 3]: cap={}", v2.capacity()); // 3
// 方式 3:Vec::with_capacity() - 预分配指定容量
let mut v3 = Vec::with_capacity(100);
println!("Vec::with_capacity(100): cap={}", v3.capacity()); // 100
// 性能对比:避免扩容
let mut v4 = Vec::with_capacity(1000);
for i in 0..1000 {
v4.push(i); // 不会触发扩容,性能更好
}
println!("预分配后: cap={}", v4.capacity()); // 1000
初始化方式对比:
| 方式 | 初始容量 | 适用场景 |
|---|---|---|
Vec::new() |
0 | 不确定大小,延迟分配 |
vec![...] |
等于元素数量 | 已知初始元素 |
Vec::with_capacity(n) |
n | 预知最终大小,避免扩容 |
为什么 Vec::new() 初始容量是 0?
- 延迟分配:如果 Vec 从未使用,不会浪费堆内存
- 零成本抽象:只有在需要时才分配内存
- 灵活性:让用户选择何时分配内存
四、Vector 的生命周期
Vector 离开作用域时会被释放
{
let v = vec![1, 2, 3, 4];
// 处理变量 v
} // <- 这里 v 离开作用域并被丢弃
重要规则:
- 类似于任何其他 struct,vector 在离开作用域时会被释放
- 当 vector 被丢弃时,所有其内容也会被丢弃
- 这意味着 vector 包含的整数将被清理
注意:一旦开始使用 vector 元素的引用,情况会变得复杂,需要遵守借用规则。
五、读取 Vector 的元素
有两种方法访问 vector 中的值:索引语法和 get 方法。
方法 1:索引语法 &v[index]
let v = vec![1, 2, 3, 4, 5];
let third: &i32 = &v[2]; // 获取第三个元素(索引从 0 开始)
println!("The third element is {}", third);
特点:
- 使用
&和[]返回一个引用 - 索引从 0 开始
- 如果索引越界,会 panic(程序崩溃)
为什么要加 &?
这里必须加 &,原因如下:
let v = vec![1, 2, 3, 4, 5];
// ❌ 错误 1:类型不匹配
// let third: i32 = &v[2];
// 错误:expected `i32`, found `&i32`
// 左边期望 i32,右边是 &i32,类型不匹配
// ❌ 错误 2:不能直接获取所有权
// let third: i32 = v[2];
// 对于没有实现 Copy 的类型会报错:cannot move out of index
// ✅ 正确写法 1:类型匹配
let third: &i32 = &v[2]; // 左边 &i32,右边 &v[2] 返回 &i32
// ✅ 正确写法 2:对于实现了 Copy 的类型(如 i32)
let third: i32 = v[2]; // 左边 i32,右边 v[2] 返回 i32(会拷贝)
类型匹配规则:
let v = vec![1, 2, 3, 4, 5];
// 表达式 v[2] 的类型是 i32
// 表达式 &v[2] 的类型是 &i32
// 规则:左边的类型必须与右边的类型匹配
let a: i32 = v[2]; // ✅ i32 = i32(拷贝)
let b: &i32 = &v[2]; // ✅ &i32 = &i32(借用)
let c: i32 = &v[2]; // ❌ i32 ≠ &i32(类型不匹配)
let d: &i32 = v[2]; // ❌ &i32 ≠ i32(类型不匹配)
深入理解类型系统:
v[2] 返回什么?
- 返回类型:i32(值本身)
- 但这个值存储在 Vec 内部
- 对于 Copy 类型:可以拷贝出来
- 对于非 Copy 类型:不能移动出来
&v[2] 返回什么?
- 返回类型:&i32(引用)
- 指向 Vec 内部的值
- 不会拷贝或移动值
- 适用于所有类型
深入理解:
v[2]的类型是什么?v[2]返回的是i32类型的值(不是引用)- 但这个值存储在 Vec 内部,属于 Vec 所有
- 为什么不能
let third: i32 = v[2]?let v = vec![1, 2, 3, 4, 5]; // ❌ 这样写会尝试"移动"值 // let third: i32 = v[2]; // 错误:cannot move out of index of `Vec<i32>`- 如果允许这样做,会将
v[2]的值移动出来 - 这会在 Vec 中留下一个”空洞”,破坏 Vec 的连续性
- Rust 不允许部分移动 Vec 的元素
重要澄清:为什么是”移动”而不是”两个指针”?
这是 Rust 与其他语言(如 Java、Python)的根本区别:
其他语言(如 Java)的做法: ┌─────────────┐ │ Vec 在堆上 │ │ [1, 2, 3] │ └─────────────┘ ↑ ↑ │ │ │ └─── third(新指针,指向同一个 3) │ └───────── v[2](原指针) 两个指针指向同一个值 ✅(Java/Python 允许) Rust 的做法: ┌─────────────┐ │ Vec 在堆上 │ │ [1, 2, ?] │ ← 如果移动,会留下空洞 └─────────────┘ ↑ │ └───────── v 仍然拥有 [1, 2, ?] third 拥有 3(移动后的值) 问题:Vec 中出现了"未初始化"的空洞 ❌(Rust 不允许)核心原因:Rust 的所有权规则
- 每个值只能有一个所有者
- 如果
let third = v[2]成功,third会成为新的所有者 - 但
v仍然认为自己拥有v[2] - 违反了”一个值只有一个所有者”的规则
- 如果
- Rust 不使用垃圾回收
- Java/Python 可以有多个引用指向同一个对象,因为有 GC
- Rust 没有 GC,必须在编译时确定谁负责释放内存
- 如果允许多个所有者,无法确定何时释放内存
- 值的表示方式不同
// 在 Rust 中,i32 是"值类型",不是"引用类型" let x: i32 = 5; // x 直接存储值 5,不是指向 5 的指针 // 在 Java 中,Integer 是"引用类型" Integer x = 5; // x 是指向堆上 Integer 对象的引用
- 如果允许这样做,会将
&v[2]做了什么?let third: &i32 = &v[2];v[2]访问 Vec 中索引为 2 的元素&v[2]获取该元素的引用(借用)- 不会移动值,只是借用查看
- 对于实现了 Copy trait 的类型呢?
let v = vec![1, 2, 3, 4, 5]; // ✅ i32 实现了 Copy,可以直接拷贝 let third: i32 = v[2]; // 这样也可以! println!("third = {}", third); // 3 // ✅ 也可以用引用 let third_ref: &i32 = &v[2]; println!("third_ref = {}", third_ref); // 3关键点:
i32实现了Copytrait,可以直接拷贝let third: i32 = v[2]会拷贝值,而不是移动- Vec 中的原值仍然存在
为什么
i32可以拷贝?Copy trait 的特殊规则: 对于实现了 Copy 的类型(如 i32): ┌─────────────┐ │ Vec 在堆上 │ │ [1, 2, 3] │ └─────────────┘ ↓ 拷贝值 3 ↓ third = 3(新的独立副本) - Vec 中仍然有 3 - third 也有 3 - 两个是独立的副本,不是指针 - 因为 i32 很小(4 字节),拷贝成本低 对于没有实现 Copy 的类型(如 String): ┌─────────────┐ │ Vec 在堆上 │ │ [String] │ ← String 包含指向堆的指针 └─────────────┘ ↓ 不能拷贝! - String 内部有指向堆的指针 - 如果简单拷贝,会有两个指针指向同一块堆内存 - 当两个 String 都被释放时,会导致 double free - 所以 String 不实现 Copy,只能移动或借用Copy vs Move vs 引用对比:
// 示例 1:i32(实现了 Copy) let v = vec![1, 2, 3]; let x = v[0]; // ✅ 拷贝:v[0] 和 x 都是独立的 1 let y = &v[0]; // ✅ 借用:y 是指向 v[0] 的引用 println!("{}", v[0]); // ✅ v[0] 仍然可用 // 示例 2:String(没有实现 Copy) let v = vec![String::from("hello")]; // let x = v[0]; // ❌ 错误:不能移动 let y = &v[0]; // ✅ 借用:y 是指向 v[0] 的引用 let z = v[0].clone(); // ✅ 克隆:创建新的 String - 对于没有实现 Copy 的类型(如 String)
let v = vec![ String::from("hello"), String::from("world"), ]; // ❌ 错误:String 没有实现 Copy,不能移动 // let first: String = v[0]; // 错误:cannot move out of index of `Vec<String>` // ✅ 正确:借用引用 let first: &String = &v[0]; println!("first = {}", first); // hello // ✅ 也可以用 clone 拷贝 let first_clone: String = v[0].clone(); println!("first_clone = {}", first_clone); // hello
总结:
| 写法 | 左边类型 | 右边类型 | 是否匹配 | 说明 |
|---|---|---|---|---|
let x: i32 = v[2] |
i32 |
i32 |
✅ | 拷贝值(仅限 Copy 类型) |
let x: &i32 = &v[2] |
&i32 |
&i32 |
✅ | 借用引用(推荐) |
let x: i32 = &v[2] |
i32 |
&i32 |
❌ | 类型不匹配 |
let x: &i32 = v[2] |
&i32 |
i32 |
❌ | 类型不匹配 |
编译错误示例:
let v = vec![1, 2, 3, 4, 5];
// ❌ 错误:类型不匹配
let third: i32 = &v[2];
// 编译错误:
// error[E0308]: mismatched types
// --> src/main.rs:3:22
// |
// 3 | let third: i32 = &v[2];
// | --- ^^^^^ expected `i32`, found `&i32`
// | |
// | expected due to this
// |
// help: consider dereferencing the borrow
// |
// 3 | let third: i32 = *&v[2];
// | +
// 解决方案 1:修改左边类型
let third: &i32 = &v[2]; // ✅
// 解决方案 2:修改右边表达式(去掉 &)
let third: i32 = v[2]; // ✅(仅限 Copy 类型)
// 解决方案 3:解引用(不推荐,多此一举)
let third: i32 = *&v[2]; // ✅ 但等价于 v[2]
类型推断:
let v = vec![1, 2, 3, 4, 5];
// Rust 可以自动推断类型,通常不需要显式标注
let third = &v[2]; // 自动推断为 &i32 ✅
let third = v[2]; // 自动推断为 i32(Copy 类型)✅
// 只有在需要明确类型时才标注
let third: &i32 = &v[2]; // 显式标注(教学目的)
// 实际开发中,推荐省略类型注解
let first = &v[0]; // ✅ 简洁,类型自动推断
let second = v[1]; // ✅ 简洁,类型自动推断
何时需要类型注解?
// 1. 编译器无法推断时(很少见)
let v = Vec::new(); // ❌ 错误:无法推断元素类型
let v: Vec<i32> = Vec::new(); // ✅ 需要类型注解
// 2. 有多种可能的类型时
let s = "42";
let n = s.parse().unwrap(); // ❌ 错误:无法推断目标类型
let n: i32 = s.parse().unwrap(); // ✅ 需要类型注解
// 3. 教学或文档目的(让代码更清晰)
let third: &i32 = &v[2]; // 显式标注,便于理解
// 4. 大多数情况下不需要
let third = &v[2]; // ✅ 推荐:简洁且类型安全
类型推断的工作原理:
let v = vec![1, 2, 3, 4, 5];
// Rust 编译器的推断过程:
let third = &v[2];
// ^^^^^ ^^^^^^
// | |
// | └─ &v[2] 的类型是 &i32
// └────────── 所以 third 的类型推断为 &i32
// 等价于:
let third: &i32 = &v[2];
最佳实践:
// ✅ 推荐:省略类型注解(简洁)
let first = &v[0];
let second = v[1];
let third = &v[2];
// ❌ 不推荐:过度标注(冗余)
let first: &i32 = &v[0];
let second: i32 = v[1];
let third: &i32 = &v[2];
// ✅ 例外:复杂类型或需要明确意图时
let numbers: Vec<i32> = Vec::new();
let result: Result<i32, ParseIntError> = s.parse();
Rust vs 其他语言的根本区别:
Java/Python/JavaScript(引用语义):
- 变量存储的是"指向对象的引用"
- 赋值操作创建新的引用,指向同一个对象
- 多个变量可以引用同一个对象
- 依赖垃圾回收器管理内存
Rust(所有权语义):
- 变量存储的是"值本身"或"唯一所有权"
- 赋值操作默认是"移动"(转移所有权)
- 每个值只有一个所有者
- 编译时确定内存释放时机,无需 GC
- 实现了 Copy 的类型例外(拷贝而非移动)
- 类型系统严格:i32 和 &i32 是完全不同的类型
为什么 Rust 这样设计?
- 内存安全:防止 double free、use after free 等问题
- 无需 GC:编译时确定内存管理,运行时零开销
- 明确所有权:清楚地知道谁负责释放内存
- 并发安全:所有权系统天然防止数据竞争
最佳实践:
- 推荐使用引用
&v[i]:适用于所有类型,不会拷贝或移动 - 如果需要拥有值:
- 基本类型:直接
v[i](自动拷贝) - 复杂类型:使用
v[i].clone()(显式拷贝)
- 基本类型:直接
方法 2:get 方法
let v = vec![1, 2, 3, 4, 5];
match v.get(2) {
Some(third) => println!("The third element is {}", third),
None => println!("There is no third element."),
}
特点:
- 返回
Option<&T>类型 - 如果索引越界,返回
None(不会 panic) - 更安全,适合处理可能越界的情况
两种方法的对比
| 方法 | 返回类型 | 索引越界行为 | 使用场景 |
|---|---|---|---|
&v[index] |
&T |
panic(程序崩溃) | 确定索引有效,越界是严重错误 |
v.get(index) |
Option<&T> |
返回 None |
索引可能无效,需要优雅处理 |
示例:索引越界的处理
let v = vec![1, 2, 3, 4, 5];
// ❌ 使用索引语法会 panic
let does_not_exist = &v[100]; // 运行时 panic!
// ✅ 使用 get 方法返回 None
let does_not_exist = v.get(100); // 返回 None,不会 panic
选择建议:
- 索引语法:当访问超出范围是严重错误时使用(应该崩溃)
get方法:当越界访问属于正常情况时使用(例如用户输入)
六、借用规则与 Vector
不能同时持有可变和不可变引用
// ❌ 这段代码不能编译!
let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0]; // 不可变借用
v.push(6); // 可变借用 ❌ 错误!
println!("The first element is: {}", first);
编译错误:
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
--> src/main.rs:6:5
|
4 | let first = &v[0];
| - immutable borrow occurs here
5 |
6 | v.push(6);
| ^^^^^^^^^ mutable borrow occurs here
7 |
8 | println!("The first element is: {}", first);
| ----- immutable borrow later used here
为什么不能这样做?
原因:vector 的工作方式导致的内存安全问题
- 当 vector 增加元素时,如果没有足够空间,会:
- 分配新的内存
- 将旧元素拷贝到新空间
- 释放旧内存
-
此时,
first引用指向的是已被释放的内存(悬垂引用) - 借用规则阻止程序陷入这种危险状况
核心规则:
- 不能在相同作用域中同时存在可变和不可变引用
- 这个规则适用于 vector 及其元素的引用
更多细节:查看 The Nomicon 了解 Vec<T> 的实现细节
七、遍历 Vector
不可变遍历
let v = vec![100, 32, 57];
for i in &v {
println!("{}", i);
}
说明:
- 使用
&v获取 vector 的不可变引用 - 遍历每个元素的不可变引用
- 只能读取,不能修改
可变遍历
let mut v = vec![100, 32, 57];
for i in &mut v {
*i += 50; // 使用解引用运算符修改值
}
说明:
- 使用
&mut v获取 vector 的可变引用 - 遍历每个元素的可变引用
- 必须使用
*解引用运算符来修改值
关键点:
i是&mut i32类型(可变引用)*i解引用后得到i32类型的值- 然后才能使用
+=运算符修改
八、使用枚举存储多种类型
问题:Vector 只能存储相同类型
Vector 的限制:只能存储相同类型的值。
解决方案:使用枚举!
枚举的优势
枚举的所有成员都是相同的枚举类型,因此可以在 vector 中存储不同类型的数据。
示例:电子表格单元格
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}
let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];
说明:
- 定义枚举
SpreadsheetCell,包含三种不同类型的成员 - 创建
Vec<SpreadsheetCell>存储不同类型的值 - 所有元素都是
SpreadsheetCell类型,满足 vector 的要求
为什么 Rust 需要在编译时知道类型?
原因 1:内存分配
- Rust 需要知道每个元素占用多少内存
- 才能正确分配和管理内存
原因 2:类型安全
- 可以准确知道 vector 中允许什么类型
- 使用枚举 +
match可以在编译时保证处理所有可能的情况
枚举方案的局限性
问题:如果在编写程序时不能确切知道运行时会存储的所有类型,枚举技术就行不通了。
解决方案:使用 trait 对象(第十七章会讲到)
九、常用方法
push - 添加元素
let mut v = vec![1, 2, 3];
v.push(4); // v = [1, 2, 3, 4]
pop - 移除并返回最后一个元素
let mut v = vec![1, 2, 3];
let last = v.pop(); // last = Some(3), v = [1, 2]
返回值:Option<T>
Some(value):如果 vector 不为空None:如果 vector 为空
其他常用方法
| 方法 | 说明 | 示例 |
|---|---|---|
len() |
返回元素数量 | v.len() |
is_empty() |
判断是否为空 | v.is_empty() |
clear() |
清空所有元素 | v.clear() |
insert(index, value) |
在指定位置插入元素 | v.insert(1, 10) |
remove(index) |
移除指定位置的元素 | v.remove(1) |
更多方法:查看标准库 Vec API 文档
十、总结
核心要点
- 创建 Vector
Vec::new():创建空 vector(需要类型注解)vec!宏:创建带初始值的 vector(推荐)
- 更新 Vector
- 使用
push添加元素 - 必须声明为
mut可变
- 使用
- 读取元素
- 索引语法
&v[index]:越界会 panic get方法:越界返回None
- 索引语法
- 借用规则
- 不能同时持有可变和不可变引用
- 防止悬垂引用和内存安全问题
- 遍历
- 不可变遍历:
for i in &v - 可变遍历:
for i in &mut v(需要*i解引用)
- 不可变遍历:
- 存储多种类型
- 使用枚举包装不同类型
- 或使用 trait 对象(高级用法)
Vector vs 数组
| 特性 | Vector Vec<T> |
数组 [T; N] |
|---|---|---|
| 大小 | 动态,可变 | 固定,编译时确定 |
| 存储位置 | 堆(数据)+ 栈(元数据) | 栈 |
| 底层实现 | 堆上的动态数组 | 栈上的固定数组 |
| 性能 | 稍慢(堆分配 + 间接访问) | 更快(栈分配 + 直接访问) |
| 灵活性 | 高(可增长) | 低(固定大小) |
| 内存开销 | 3 个 usize(栈)+ 数据(堆) | 仅数据本身 |
内存布局对比
// 数组:所有数据在栈上
let arr = [1, 2, 3];
// 栈: [1, 2, 3] <- 12 字节(3 × 4 字节)
// Vector:元数据在栈上,数据在堆上
let vec = vec![1, 2, 3];
// 栈: [ptr, len=3, cap=3] <- 24 字节(3 × 8 字节,64位系统)
// |
// v
// 堆: [1, 2, 3] <- 12 字节(3 × 4 字节)
最佳实践
- 优先使用
vec!宏:代码更简洁,类型推断自动 - 预分配容量:如果知道最终大小,使用
Vec::with_capacity(n)避免扩容 - 选择合适的访问方法:确定索引有效用
[],可能越界用get - 注意借用规则:避免在持有引用时修改 vector(可能触发扩容导致引用失效)
- 使用枚举存储多种类型:保持类型安全
- 查阅 API 文档:Vector 有很多实用方法
性能优化建议
- 避免不必要的扩容
// ❌ 不好:多次扩容 let mut v = Vec::new(); for i in 0..1000 { v.push(i); // 可能触发多次扩容 } // ✅ 好:预分配容量 let mut v = Vec::with_capacity(1000); for i in 0..1000 { v.push(i); // 不会扩容 } - 使用迭代器而非索引
// ❌ 不好:索引访问 for i in 0..v.len() { println!("{}", v[i]); } // ✅ 好:迭代器(更快,更安全) for item in &v { println!("{}", item); } - 避免不必要的克隆
// ❌ 不好:克隆整个 vector let v2 = v.clone(); // ✅ 好:使用引用 let v2 = &v;