《Rust 编程实战》系列第 19 篇
在上一篇文章中,我们学习了 String 与 &str。
这一篇继续学习 Rust 中最常用的集合类型之一:
r
Vec<T>
Vec 通常称为动态数组,也叫向量。
与普通数组不同,Vec 的长度可以在程序运行过程中变化。我们可以动态添加、删除和修改元素。
例如:
ini
let mut numbers = vec![10, 20, 30];
numbers.push(40);
执行后,numbers 变成:
csharp
[10, 20, 30, 40]
本文将介绍:
-
什么是
Vec -
创建动态数组
-
添加和删除元素
-
访问和修改元素
-
遍历
Vec -
len()与capacity() -
排序、查找和去重
-
Vec<String>的所有权 -
Vec、数组和切片的区别 -
常见错误
-
实战案例
什么是 Vec?
Vec<T> 是 Rust 标准库提供的可增长集合。
它具有以下特点:
-
所有元素必须是相同类型
-
元素按照插入顺序排列
-
长度可以动态变化
-
数据通常存储在堆上
-
支持添加、删除、排序和查找
例如:
rust
fn main() {
let numbers = vec![10, 20, 30];
println!("{:?}", numbers);
}
输出:
csharp
[10, 20, 30]
这里 numbers 的类型通常是:
rust
Vec<i32>
使用 vec! 宏创建 Vec
最常用的创建方式是:
css
vec![]
例如:
rust
fn main() {
let fruits = vec!["苹果", "香蕉", "橙子"];
println!("{:?}", fruits);
}
也可以显式声明类型:
rust
fn main() {
let scores: Vec<i32> = vec![80, 90, 100];
println!("{:?}", scores);
}
Rust 通常可以根据元素自动推断类型。
创建空 Vec
可以使用:
arduino
Vec::new()
例如:
rust
fn main() {
let numbers: Vec<i32> = Vec::new();
println!("{:?}", numbers);
}
创建空 Vec 时,通常必须指定元素类型,因为编译器无法从空集合推断。
也可以写成:
ruby
let numbers = Vec::<i32>::new();
使用 Vec::with_capacity()
如果提前知道大概会保存多少元素,可以预先分配容量:
rust
fn main() {
let numbers: Vec<i32> =
Vec::with_capacity(100);
println!("长度:{}", numbers.len());
println!("容量:{}", numbers.capacity());
}
输出类似:
长度:0
容量:100
这不会创建 100 个元素,只会提前准备容纳 100 个元素的空间。
使用重复值创建 Vec
可以使用:
css
vec![值; 数量]
例如:
rust
fn main() {
let numbers = vec![0; 5];
println!("{:?}", numbers);
}
输出:
csharp
[0, 0, 0, 0, 0]
这种写法适合初始化状态、计数器或缓冲区。
使用 push() 添加元素
Vec 默认不可变。
如果需要添加元素,必须使用 mut:
ini
fn main() {
let mut numbers = vec![10, 20];
numbers.push(30);
numbers.push(40);
println!("{:?}", numbers);
}
输出:
csharp
[10, 20, 30, 40]
push() 会把元素添加到末尾。
新元素类型必须与原有元素类型一致。
使用 pop() 删除末尾元素
pop() 会删除并返回最后一个元素。
ini
fn main() {
let mut numbers = vec![10, 20, 30];
let value = numbers.pop();
println!("删除结果:{:?}", value);
println!("剩余数据:{:?}", numbers);
}
输出:
scss
删除结果:Some(30)
剩余数据:[10, 20]
pop() 返回:
r
Option<T>
因为空 Vec 没有元素可以删除。
javascript
if let Some(value) = numbers.pop() {
println!("删除了:{}", value);
}
使用 insert() 插入元素
可以在指定索引位置插入元素:
ini
fn main() {
let mut numbers = vec![10, 30, 40];
numbers.insert(1, 20);
println!("{:?}", numbers);
}
输出:
csharp
[10, 20, 30, 40]
原来索引 1 及其后面的元素会向后移动。
如果插入位置超过长度,程序会 panic。
使用 remove() 删除指定元素
rust
fn main() {
let mut fruits =
vec!["苹果", "香蕉", "橙子"];
let removed = fruits.remove(1);
println!("删除:{}", removed);
println!("剩余:{:?}", fruits);
}
输出:
css
删除:香蕉
剩余:["苹果", "橙子"]
remove() 会移动后面的元素,因此频繁从开头或中间删除可能成本较高。
使用 swap_remove() 快速删除
如果不关心元素顺序,可以使用:
scss
swap_remove()
ini
fn main() {
let mut numbers = vec![10, 20, 30, 40];
let removed = numbers.swap_remove(1);
println!("删除:{}", removed);
println!("剩余:{:?}", numbers);
}
输出可能是:
css
删除:20
剩余:[10, 40, 30]
它会把最后一个元素移动到删除位置,因此速度更快,但会改变顺序。
访问 Vec 元素
可以使用索引访问:
ini
fn main() {
let numbers = vec![10, 20, 30];
println!("{}", numbers[0]);
println!("{}", numbers[1]);
}
索引从 0 开始。
如果访问越界索引:
arduino
println!("{}", numbers[10]);
程序会 panic。
使用 get() 安全访问
当索引不确定时,推荐使用:
scss
get()
ini
fn main() {
let numbers = vec![10, 20, 30];
let index = 5;
match numbers.get(index) {
Some(value) => {
println!("元素:{}", value);
}
None => {
println!("索引超出范围");
}
}
}
get() 返回:
r
Option<&T>
因此不会因为越界直接崩溃。
获取第一个和最后一个元素
rust
fn main() {
let numbers = vec![10, 20, 30];
println!("{:?}", numbers.first());
println!("{:?}", numbers.last());
}
输出:
scss
Some(10)
Some(30)
两者都返回 Option<&T>,因为集合可能为空。
修改 Vec 元素
ini
fn main() {
let mut numbers = vec![10, 20, 30];
numbers[1] = 200;
println!("{:?}", numbers);
}
输出:
csharp
[10, 200, 30]
也可以使用:
scss
get_mut()
安全获得可变引用:
ini
fn main() {
let mut numbers = vec![10, 20, 30];
if let Some(value) = numbers.get_mut(1) {
*value = 200;
}
println!("{:?}", numbers);
}
遍历 Vec
最常见的方式是使用 for:
rust
fn main() {
let numbers = vec![10, 20, 30];
for number in &numbers {
println!("{}", number);
}
println!("{:?}", numbers);
}
使用:
yaml
&numbers
可以借用集合,遍历后原 Vec 仍然可以使用。
遍历并修改元素
使用可变引用:
rust
fn main() {
let mut numbers = vec![10, 20, 30];
for number in &mut numbers {
*number *= 2;
}
println!("{:?}", numbers);
}
输出:
csharp
[20, 40, 60]
循环变量是可变引用,因此需要使用:
typescript
*number
进行解引用。
获取索引和值
可以使用:
scss
enumerate()
javascript
fn main() {
let fruits =
vec!["苹果", "香蕉", "橙子"];
for (index, fruit) in
fruits.iter().enumerate()
{
println!("{}:{}", index, fruit);
}
}
输出:
0:苹果
1:香蕉
2:橙子
len() 与 is_empty()
获取当前元素数量:
go
numbers.len()
判断是否为空:
scss
numbers.is_empty()
示例:
rust
fn main() {
let numbers = vec![10, 20, 30];
println!("长度:{}", numbers.len());
println!("是否为空:{}", numbers.is_empty());
}
长度与容量的区别
Vec 有两个重要概念:
go
长度 len
容量 capacity
长度表示当前实际元素数量。
容量表示当前已分配空间最多可以容纳多少个元素,而不需要重新分配内存。
rust
fn main() {
let mut numbers = Vec::new();
println!(
"长度:{},容量:{}",
numbers.len(),
numbers.capacity()
);
for number in 1..=10 {
numbers.push(number);
println!(
"长度:{},容量:{}",
numbers.len(),
numbers.capacity()
);
}
}
当长度超过容量时,Vec 会自动申请更大的内存空间。
具体扩容策略属于实现细节,不应该依赖固定倍数。
预留和释放容量
可以使用:
scss
reserve()
提前增加容量:
ini
numbers.reserve(100);
如果想尽量释放多余容量,可以使用:
scss
shrink_to_fit()
ini
fn main() {
let mut numbers =
Vec::with_capacity(100);
numbers.push(10);
numbers.push(20);
numbers.shrink_to_fit();
println!("{}", numbers.capacity());
}
不过频繁调整容量也有成本,通常不需要手动干预。
清空 Vec
使用:
scss
clear()
rust
fn main() {
let mut numbers = vec![10, 20, 30];
numbers.clear();
println!("{:?}", numbers);
println!("长度:{}", numbers.len());
}
clear() 会删除所有元素,但已分配容量通常会保留,以便以后继续使用。
判断是否包含元素
rust
fn main() {
let numbers = vec![10, 20, 30];
println!("{}", numbers.contains(&20));
println!("{}", numbers.contains(&99));
}
输出:
arduino
true
false
查找元素位置
可以结合迭代器和 position():
ini
fn main() {
let numbers = vec![10, 20, 30, 20];
let position = numbers
.iter()
.position(|value| *value == 20);
println!("{:?}", position);
}
输出:
scss
Some(1)
只返回第一个匹配位置。
保留符合条件的元素
可以使用:
scss
retain()
ini
fn main() {
let mut numbers =
vec![1, 2, 3, 4, 5, 6];
numbers.retain(|number| {
number % 2 == 0
});
println!("{:?}", numbers);
}
输出:
csharp
[2, 4, 6]
retain() 会保留闭包返回 true 的元素。
排序 Vec
整数升序排序:
ini
fn main() {
let mut numbers = vec![30, 10, 50, 20];
numbers.sort();
println!("{:?}", numbers);
}
输出:
csharp
[10, 20, 30, 50]
降序排序:
css
numbers.sort_by(|a, b| b.cmp(a));
完整示例:
css
fn main() {
let mut numbers = vec![30, 10, 50, 20];
numbers.sort_by(|a, b| b.cmp(a));
println!("{:?}", numbers);
}
浮点数排序
浮点数不能直接使用普通 sort(),因为其中可能存在 NaN。
可以使用:
scss
sort_by()
css
fn main() {
let mut prices =
vec![29.9, 10.5, 88.0, 35.6];
prices.sort_by(|a, b| {
a.partial_cmp(b).unwrap()
});
println!("{:?}", prices);
}
正式项目中需要考虑 NaN,避免随意使用 unwrap()。
去除重复元素
dedup() 只会删除相邻的重复元素。
ini
fn main() {
let mut numbers =
vec![1, 2, 2, 3, 1, 1];
numbers.dedup();
println!("{:?}", numbers);
}
结果:
csharp
[1, 2, 3, 1]
如果要删除所有重复值,可以先排序:
ini
fn main() {
let mut numbers =
vec![3, 1, 2, 3, 2, 1];
numbers.sort();
numbers.dedup();
println!("{:?}", numbers);
}
输出:
csharp
[1, 2, 3]
使用 append() 合并 Vec
rust
fn main() {
let mut first = vec![1, 2, 3];
let mut second = vec![4, 5, 6];
first.append(&mut second);
println!("first:{:?}", first);
println!("second:{:?}", second);
}
输出:
css
first:[1, 2, 3, 4, 5, 6]
second:[]
append() 会把 second 中的元素移动到 first。
使用 extend() 添加多个元素
ini
fn main() {
let mut numbers = vec![1, 2];
numbers.extend([3, 4, 5]);
println!("{:?}", numbers);
}
输出:
csharp
[1, 2, 3, 4, 5]
Vec 与切片
Vec<T> 可以方便地借用为:
css
&[T]
例如:
rust
fn calculate_total(numbers: &[i32]) -> i32 {
numbers.iter().sum()
}
fn main() {
let numbers = vec![10, 20, 30];
println!("{}", calculate_total(&numbers));
}
函数只需要读取数据时,使用切片参数比直接接收 Vec 更通用。
同一个函数既可以接收数组,也可以接收 Vec:
ini
let array = [1, 2, 3];
let vector = vec![4, 5, 6];
calculate_total(&array);
calculate_total(&vector);
Vec 与所有权
Vec 可以保存 String:
rust
fn main() {
let names = vec![
String::from("Tom"),
String::from("Alice"),
];
println!("{:?}", names);
}
不能直接把元素从索引位置移动出来:
ini
let name = names[0];
因为索引操作通常返回引用位置,而不能破坏集合结构。
可以借用:
ini
let name = &names[0];
也可以克隆:
ini
let name = names[0].clone();
或者通过 remove() 取得所有权:
rust
fn main() {
let mut names = vec![
String::from("Tom"),
String::from("Alice"),
];
let name = names.remove(0);
println!("{}", name);
}
数组、Vec 与切片的区别
| 特性 | Array | Vec | Slice |
|---|---|---|---|
| 长度 | 固定 | 动态 | 由引用范围决定 |
| 是否拥有数据 | 是 | 是 | 否 |
| 添加元素 | 不支持 | 支持 | 不支持 |
| 删除元素 | 不支持 | 支持 | 不支持 |
| 常见类型 | [T; N] |
Vec<T> |
&[T] |
| 使用场景 | 固定数量 | 动态集合 | 借用连续数据 |
可以简单理解:
Array 是固定长度容器。
Vec 是动态长度容器。
Slice 是对容器中一段数据的借用。
实战:商品列表管理
定义一个商品名称列表,支持添加、删除和查找:
rust
fn main() {
let mut products = vec![
String::from("键盘"),
String::from("鼠标"),
String::from("显示器"),
];
products.push(
String::from("耳机"),
);
if let Some(position) = products
.iter()
.position(|name| name == "鼠标")
{
let removed = products.remove(position);
println!("删除商品:{}", removed);
}
for (index, product) in
products.iter().enumerate()
{
println!(
"{}. {}",
index + 1,
product
);
}
}
输出:
markdown
删除商品:鼠标
1. 键盘
2. 显示器
3. 耳机
实战:成绩统计
rust
fn analyze_scores(
scores: &[i32],
) -> Option<(i32, f64, i32, i32)> {
if scores.is_empty() {
return None;
}
let total: i32 = scores.iter().sum();
let average =
total as f64 / scores.len() as f64;
let min = *scores.iter().min()?;
let max = *scores.iter().max()?;
Some((total, average, min, max))
}
fn main() {
let mut scores = vec![88, 59, 95, 76];
scores.push(100);
scores.retain(|score| *score >= 60);
scores.sort_by(|a, b| b.cmp(a));
match analyze_scores(&scores) {
Some((total, average, min, max)) => {
println!("成绩:{:?}", scores);
println!("总分:{}", total);
println!("平均分:{:.2}", average);
println!("最低分:{}", min);
println!("最高分:{}", max);
}
None => {
println!("没有成绩数据");
}
}
}
输出:
css
成绩:[100, 95, 88, 76]
总分:359
平均分:89.75
最低分:76
最高分:100
实战:简单任务队列
rust
fn main() {
let mut tasks = vec![
String::from("学习 Vec"),
String::from("编写练习"),
];
tasks.push(
String::from("复习所有权"),
);
println!("当前任务:");
for (index, task) in
tasks.iter().enumerate()
{
println!("{}. {}", index + 1, task);
}
if !tasks.is_empty() {
let finished = tasks.remove(0);
println!("完成任务:{}", finished);
}
println!("剩余任务:{:?}", tasks);
}
需要频繁从队列开头删除元素时,实际项目通常更适合使用:
VecDeque
因为 Vec::remove(0) 会移动后面的所有元素。
常见错误
忘记声明 mut
错误:
ini
let numbers = vec![1, 2, 3];
numbers.push(4);
正确:
ini
let mut numbers = vec![1, 2, 3];
numbers.push(4);
空 Vec 没有指定类型
错误:
ini
let values = Vec::new();
如果后面没有足够信息帮助推断类型,编译器会报错。
正确:
rust
let values: Vec<i32> = Vec::new();
使用索引访问越界元素
错误:
ini
let numbers = vec![1, 2, 3];
println!("{}", numbers[10]);
不确定索引是否合法时,应使用:
csharp
numbers.get(10)
遍历时同时修改 Vec 结构
下面的思路通常会产生借用冲突:
typescript
for number in &numbers {
numbers.push(*number);
}
遍历期间已经不可变借用了 numbers,不能同时进行可变修改。
可以先收集新元素,再统一添加。
误以为 dedup() 会删除所有重复值
ini
let mut values = vec![1, 2, 1];
values.dedup();
结果仍然是:
csharp
[1, 2, 1]
因为重复值不相邻。
如果不关心原顺序,可以先排序再去重。
频繁 remove(0)
csharp
values.remove(0);
每次都会移动后面的元素。
如果业务是先进先出的队列,应考虑:
VecDeque
最佳实践
函数只读集合时使用切片
推荐:
rust
fn print_values(values: &[i32]) {
println!("{:?}", values);
}
而不是:
rust
fn print_values(values: &Vec<i32>) {
}
切片参数更通用。
已知数据量时预分配容量
ini
let mut records =
Vec::with_capacity(1000);
可以减少扩容次数,但不需要为很小的数据过度优化。
不确定索引时使用 get()
csharp
if let Some(value) = values.get(index) {
println!("{}", value);
}
不关心顺序时使用 swap_remove()
ini
values.swap_remove(index);
它通常比 remove() 更高效,但会改变元素顺序。
多个业务字段优先使用结构体
不推荐长期使用:
rust
Vec<(u64, String, f64, bool)>
更清晰的写法是:
rust
struct Product {
id: u64,
name: String,
price: f64,
enabled: bool,
}
然后使用:
rust
Vec<Product>
本章小结
Vec<T> 是 Rust 中最常用的动态集合之一。
通过本文,我们学习了:
-
使用
vec![]创建动态数组 -
使用
Vec::new()创建空集合 -
push()、pop()、insert()和remove() -
使用索引和
get()访问元素 -
使用
get_mut()修改元素 -
遍历和可变遍历
-
len()、capacity()和动态扩容 -
clear()、retain()和contains() -
排序、查找与去重
-
append()和extend() -
Vec<String>的所有权问题 -
Vec与数组、切片的区别 -
商品列表、成绩统计和任务队列案例
可以记住:
固定数量的数据使用数组。
动态变化的数据使用Vec。
函数只需要读取集合时,参数优先使用切片&[T]。
下一篇预告
下一篇我们将学习 Rust HashMap 哈希表详解,包括:
-
什么是
HashMap -
创建哈希表
-
插入和更新键值
-
通过键查询数据
-
get()与安全访问 -
删除键值
-
遍历
HashMap -
entry()API -
or_insert()与计数统计 -
String所有权问题 -
HashMap与Vec的区别 -
实战:用户信息、商品库存和单词统计