Vector
约 1782 字大约 6 分钟
2025-08-03
Vector 在 Rust 中是一个可动态扩容的数组,它提供了一个 连续的内存块 来存储元素,并且可以根据需要自动调整大小。通常使用 Vec<T> 来表示一个存储类型为 T 的 Vec。
重要
Vec 只能储存相同类型的值
创建 Vec
Vec::new 与 vec!
创建一个空的 Vec,可以调用 Vec::new 函数,也可以通过宏 vec! 来创建并初始化 Vec:
let vec: Vec<i32> = Vec::new();let vec = vec![1, 2, 3];笔记
使用宏创建 Vec 时,Rust 会自动推断出 Vec 的类型
Vec::with_capacity
默认情况下,Vec 在容量不足时会自动扩容,虽然这种机制对使用者透明,但频繁扩容会带来额外的内存分配与数据复制成本。如果提前知道大概会存储多少元素,可以使用 Vec::with_capacity 预分配容量,从而减少扩容次数
fn main() {
let mut v = Vec::with_capacity(10);
println!("len = {}", v.len());
println!("capacity = {}", v.capacity());
v.push(1);
v.push(2);
println!("{v:?}");
}
with_capacity只会提前分配容量,不会直接创建元素
读取 Vec
通过索引访问
可以使用索引语法 v[index] 来访问 vector 中的元素,返回一个对该元素的引用
索引访问 Vec
fn main() {
let v = vec![1, 2, 3, 4, 5];
let third: &i32 = &v[2];
println!("The third element by index is {}", third);
}为什么这里要用 &
使用 & 借用符号,涉及到 "所有权" 和 "是否移动值"。Vec 不希望默认把值移动出去,假设:
let v = vec! [
String::from("hello"),
String::from("world"),
]如果允许:let x = v[0];,那就意味着会把 String 移动出去,从而导致 Vec 内部数据不完整。
但如果是 i32、bool 等实现了 Copy Trait 的类型,则会直接复制值,不涉及所有权转移,因此可以不使用 &。
如果索引越界会直接 panic,因此需要确保索引有效
通过 get 方法访问
也可以使用 get 方法,它会返回一个 Option<&T>,通过 Option 中的 None 枚举显式处理元素不存在的情况;相比索引更安全,但写法略长
get 方法访问 Vec
fn main() {
let v = vec![1, 2, 3, 4, 5];
let third: Option<&i32> = v.get(2);
match third {
Some(third) =>
println!("The third element by get is {}", third),
None => println!("There is no third element."),
}
}通常 get 方法只获取只读引用,如果需要修改元素则使用 get_mut 获取可变引用
fn main() {
let mut v2 = vec![10, 20, 30];
if let Some(x) = v2.get_mut(0) {
*x += 1;
}
println!("v2 after get_mut: {v2:?}");
}遍历 Vec
for 循环遍历
如果想要依次访问 vector 中的每一个元素,可以使用 for 循环来遍历这个 vector。根据需要选择按值、按引用或按可变引用遍历,决定所有权去向以及是否修改元素
fn main() {
let v = vec![100, 32, 57];
for i in &v {
println!("{i}");
}
}fn main() {
let mut v = vec![100, 32, 57];
for i in &mut v {
*i += 50;
}
}iter/iter_mut/into_iter 方法遍历
Rust 遍历 Vec 时,不仅仅是 "如何循环" 的问题,更重要的是:元素是被借用、可变借用,还是直接移动所有权。因此 Rust 提供了三种不同的迭代器
iter:返回元素的不可变引用,适合只读访问
fn main() {
let v = vec![1, 2, 3];
for item in v.iter() {
println!("{item}");
}
println!("{v:?}");
}遍历结束后
v依然可用
iter_mut:返回元素的可变引用,适合需要修改元素的场景
fn main() {
let mut v = vec![1, 2, 3];
for item in v.iter_mut() {
*item += 10;
}
println!("{v:?}");
}这里
item的类型为&mut i32,因此需要解引用才能修改值
into_iter:会直接获取 Vec 的所有权,并移动其中的元素
fn main() {
let v = vec![1, 2, 3];
for item in v.into_iter() {
println!("{item}");
}
// println!("{v:?}");
}
v的所有权已被移动,遍历结束后v不再可用
借用检查规则
Vec<T> 的底层本质可以理解为三个核心字段:
ptr:指向堆上连续内存的指针len:当前元素数量capacity:已分配的容量
例如:
let mut v = vec![1, 2, 3];此时 len = 3,capacity >= 3。当调用 push 时,如果当前容量足够,那么元素会直接写入当前内存空间;如果容量不足则会触发扩容,申请一块更大的内存,复制原有数据到新内存中,释放旧内存,最后更新指针和容量。
因此,Vec 扩容后,元素的内存地址可能发生变化,如果在迭代过程中修改 Vec,可能会导致悬垂引用。
fn main() {
let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0];
// 在借用元素期间修改 Vec 会报错
// v.push(6);
println!("The first element is: {first}");
}使用枚举来存储多种类型
如果需要在一个 Vec 中表示多种不同类型的数据,可以使用 enum 把多种可能的数据形态统一成一个类型,这样 Vec 就可以通过 Vec<Enum> 的形式间接存储多种数据
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}
fn main() {
let row: Vec<SpreadsheetCell> = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];
}实际使用时需要用模式匹配区分变体:
匹配枚举变体
fn main() {
let row: Vec<SpreadsheetCell> = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];
for cell in row {
match cell {
SpreadsheetCell::Int(i) => println!("int: {i}"),
SpreadsheetCell::Float(f) => println!("float: {f}"),
SpreadsheetCell::Text(s) => println!("text: {s}"),
}
}
}其实本质上它不是 JS 中数组对象的概念:
[{}, {}]。在这个例子中,它依然存储的是三个元素,只不过借助enum的能力使得 Vec 存储的元素类型变得多样化了。
VecDeque
VecDeque 是 Rust 标准库中提供的一个双端队列(Double-Ended Queue)数据结构,位于 std::collections 模块中。
与 Vec 不同,VecDeque 允许在两端高效地添加和删除元素,而 Vec 只能在末尾高效地添加元素。
重要
Vec 只是在末尾添加元素时效率高,而一旦在头部或者中间插入元素时就需要移动大量元素,效率较低;而 VecDeque 在两端添加或删除元素时都能保持高效
使用 VecDeque
use std::collections::VecDeque;
fn main() {
let mut queue = VecDeque::new();
queue.push_back(1);
queue.push_back(2);
queue.push_front(0);
println!("{queue:?}"); // [0, 1, 2]
let first = queue.pop_front();
println!("{first:?}"); // Some(0)
let last = queue.pop_back();
println!("{last:?}"); // Some(2)
}底层原理
VecDeque 使用一个环形缓冲区,可以理解为它有一块数组空间,但是头尾可以 "绕圈",比如容量是 8:
下标: 0 1 2 3 4 5 6 7
数据: _ _ A B C _ _ _
↑ ↑
front back如果在头部插入,不需要把 A B C 整体后移,只需要把 front 往前挪:
下标: 0 1 2 3 4 5 6 7
数据: _ X A B C _ _ _
↑ ↑
front back如果前面没位置了,还可以绕到数组末尾:
下标: 0 1 2 3 4 5 6 7
数据: A B C _ _ _ _ X
↑ ↑
back front