rust 文档 - 集合 - Vector 动态数组详解

 

学习资源

  • 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 的三个核心字段

  1. ptr(指针):指向堆上数组的起始地址
  2. len(长度):当前已存储的元素数量
  3. 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 内部的值
- 不会拷贝或移动值
- 适用于所有类型

深入理解

  1. v[2] 的类型是什么?
    • v[2] 返回的是 i32 类型的(不是引用)
    • 但这个值存储在 Vec 内部,属于 Vec 所有
  2. 为什么不能 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 的所有权规则

    1. 每个值只能有一个所有者
      • 如果 let third = v[2] 成功,third 会成为新的所有者
      • v 仍然认为自己拥有 v[2]
      • 违反了”一个值只有一个所有者”的规则
    2. Rust 不使用垃圾回收
      • Java/Python 可以有多个引用指向同一个对象,因为有 GC
      • Rust 没有 GC,必须在编译时确定谁负责释放内存
      • 如果允许多个所有者,无法确定何时释放内存
    3. 值的表示方式不同
      // 在 Rust 中,i32 是"值类型",不是"引用类型"
      let x: i32 = 5;
      // x 直接存储值 5,不是指向 5 的指针
      
      // 在 Java 中,Integer 是"引用类型"
      Integer x = 5;
      // x 是指向堆上 Integer 对象的引用
      
  3. &v[2] 做了什么?
    let third: &i32 = &v[2];
    
    • v[2] 访问 Vec 中索引为 2 的元素
    • &v[2] 获取该元素的引用(借用)
    • 不会移动值,只是借用查看
  4. 对于实现了 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 实现了 Copy trait,可以直接拷贝
    • 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
    
  5. 对于没有实现 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 这样设计?

  1. 内存安全:防止 double free、use after free 等问题
  2. 无需 GC:编译时确定内存管理,运行时零开销
  3. 明确所有权:清楚地知道谁负责释放内存
  4. 并发安全:所有权系统天然防止数据竞争

最佳实践

  • 推荐使用引用 &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 的工作方式导致的内存安全问题

  1. 当 vector 增加元素时,如果没有足够空间,会:
    • 分配新的内存
    • 将旧元素拷贝到新空间
    • 释放旧内存
  2. 此时,first 引用指向的是已被释放的内存(悬垂引用)

  3. 借用规则阻止程序陷入这种危险状况

核心规则

  • 不能在相同作用域中同时存在可变和不可变引用
  • 这个规则适用于 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 文档


十、总结

核心要点

  1. 创建 Vector
    • Vec::new():创建空 vector(需要类型注解)
    • vec! 宏:创建带初始值的 vector(推荐)
  2. 更新 Vector
    • 使用 push 添加元素
    • 必须声明为 mut 可变
  3. 读取元素
    • 索引语法 &v[index]:越界会 panic
    • get 方法:越界返回 None
  4. 借用规则
    • 不能同时持有可变和不可变引用
    • 防止悬垂引用和内存安全问题
  5. 遍历
    • 不可变遍历:for i in &v
    • 可变遍历:for i in &mut v(需要 *i 解引用)
  6. 存储多种类型
    • 使用枚举包装不同类型
    • 或使用 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 字节)

最佳实践

  1. 优先使用 vec!:代码更简洁,类型推断自动
  2. 预分配容量:如果知道最终大小,使用 Vec::with_capacity(n) 避免扩容
  3. 选择合适的访问方法:确定索引有效用 [],可能越界用 get
  4. 注意借用规则:避免在持有引用时修改 vector(可能触发扩容导致引用失效)
  5. 使用枚举存储多种类型:保持类型安全
  6. 查阅 API 文档:Vector 有很多实用方法

性能优化建议

  1. 避免不必要的扩容
    // ❌ 不好:多次扩容
    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);  // 不会扩容
    }
    
  2. 使用迭代器而非索引
    // ❌ 不好:索引访问
    for i in 0..v.len() {
        println!("{}", v[i]);
    }
    
    // ✅ 好:迭代器(更快,更安全)
    for item in &v {
        println!("{}", item);
    }
    
  3. 避免不必要的克隆
    // ❌ 不好:克隆整个 vector
    let v2 = v.clone();
    
    // ✅ 好:使用引用
    let v2 = &v;
    

参考资料