使用向量存储值列表
我们首先要了解的集合类型是 Vec<T>
,也称为向量。向量允许你在单个数据结构中存储多个值,这些值在内存中是连续排列的。向量只能存储相同类型的值。当你有一系列项目时,比如文件中的文本行或购物车中的商品价格,向量非常有用。
创建新向量
要创建一个新的空向量,我们调用 Vec::new 函数,如清单 8-1 所示。
rust
let v: Vec<i32> = Vec::new();
清单 8-1:创建一个用于保存 i32 类型值的新空向量
注意这里添加了类型注解。因为我们没有往这个向量里插入任何值,Rust 不知道我们打算存储什么类型的元素。这一点很重要。向量是通过泛型实现的;第10章会介绍如何对自定义类型使用泛型。目前只需知道标准库提供的 Vec<T>
类型可以容纳任意类型。当我们创建特定类型的向量时,可以在尖括号内指定该类型。在清单 8-1 中,我们告诉 Rust v 中的 Vec<T>
将保存 i32 类型元素。
通常,你会用初始值来创建 Vec<T>
,Rust 会推断出你想要存储的数据类型,因此很少需要写这种类型注解。Rust 提供了方便的 vec! 宏,它会根据给定的值创建一个新的向量。清单 8-2 创建了一个包含 1、2 和 3 的新 Vec<i32>
向量。整数默认就是 i32 类型,如第3章"数据类型"部分所述。
rust
let v = vec![1, 2, 3];
清单 8-2:创建包含初始值的新向量
由于给出了初始 i32 值,Rust 能推断出 v 的类型为 Vec<i32>
,因此不需要额外标注。
更新向量
如果先创建空 vector,然后再添加元素,可以使用 push 方法,如清单 8-3 所示:
rust
let mut v = Vec::new();
v.push(5);
v.push(6);
v.push(7);
v.push(8);
清单 8-3:使用 push 方法将数值添加到 vector
和所有变量一样,如果想修改其内容,需要用 mut 标记它为可变(详见第3章)。这里放入的是 i32 类型数字,Rust 根据数据自动推断,所以无需显式写成 Vec<i32>
。
读取 Vector 元素
访问 vector 中某个元素,有两种方式:索引访问和 get 方法访问。在下面例子中,为了更明确地说明返回结果,也加上了返回值得具体类型注释。
清单 8-4 展示了这两种方法:
rust
let v = vec![1, 2, 3, 4, 5];
let third: &i32 = &v[2];
println!("第三个元素是 {third}");
let third: Option<&i32> = v.get(2);
match third {
Some(third) => println!("第三个元素是 {third}"),
None => println!("没有第三个元素"),
}
清单 8-4:通过索引语法和 get 方法访问 vector 项目
几点说明:
索引从0开始,因此索引为2表示第三个元素。
& 和 [] 返回的是指针引用。
get 方法传入索引后返回 Option<&T>, 可以配合 match 使用。
Rust 提供这两种方式,是为了让程序员选择当越界访问时程序行为------直接 panic 或优雅处理。例如看下尝试获取长度仅五项但请求100号位置,会发生什么(见 清单 8-5):
rust
let v = vec![1,2,3,4,5];
let does_not_exist = &v[100]; // 索引越界,将导致 panic!
let does_not_exist = v.get(100); // 返回 None,不会崩溃
清单 8-5:尝试访问只有五项vector里的第100号位置
第一种[]方法遇到不存在的位置会使程序崩溃;适合希望严格保证不会越界场景。
第二种get方法则安全得多,会返回None,可编写逻辑处理异常情况,例如用户输入超范围数字时提示重新输入,更友好且稳定!
借用检查器确保有效引用规则生效(详见第4章),禁止同时存在可变与不可变引用。如以下代码(见 清单 8-6):
rust
let mut v=vec![1,2,3,4,5];
let first=&v[0];
v.push(6);
println!("第一个元素是: {first}");
清单 8-6 :持有不可变引用期间尝试修改vector末尾内容
此代码无法编译,通过错误信息指出不能同时拥有不可变借用和可变借用,因为push可能触发内存重分配,使之前持有的不变引用失效。这体现了 Rust 内部对连续内存布局及安全性的保障机制。(更多细节请参阅《The Rustonomicon》)
遍历 Vector 中所有数值
若想依次操作每个元素,而非按索引逐一取,用 for 循环迭代即可。如打印每项(见 清單8-7):
rust
let v=vec![100 ,32 ,57 ];
for i in &v {
println!("{i}");
}
若需修改每项,则迭代可变引用,并利用 * 解引用符改变实际数值 (见 清單8-8):
rust
let mut v=vec![100 ,32 ,57 ];
for i in &mut v {
*i +=50;
}
无论是否可变遍历,都符合借用检查器规则。如果循环体内部插入或删除项目,将产生类似前面提到错误,因为循环本身保持着对整个vector的一致性控制权限制并发改动。
使用枚举支持多种不同数据类混合保存
Vec只能装同一种类的数据,但现实需求常常需要混合不同类别对象一起管理。这时候可以定义枚举(enum),把各种可能的数据包装成统一enum成员,再构建Vec<该enum>
例如表格行里既有整数、浮点、小段文字等字段,就能这样做 (见 清單8-9):
rust
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}
let row=vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12)
];
这样就能把不同底层数据封装进同一Vector,同时享受静态强制检查带来的安全性与性能优势。但必须提前确定所有可能出现的数据类别,否则无法满足编译期完整匹配要求。如果运行时才动态决定未知类别,则应考虑 trait 对象方案,第18章讲解相关内容。
总结一下,本章节介绍了一些最常用且实战价值极高的方法来操作Vec,请务必查阅官方API文档以掌握更多丰富功能,比如 pop 用于移除并返回最后一项等辅助函数。
销毁 Vector 时,其内部所有内容也随之释放
像其他结构体一样,当Vector离开作用域即被丢弃(drop),其所含全部资源都会被回收,包括其中保存的数据。(如 清單8-10)
rust
{
let v=vec![1 ,2 ,3 ,4 ];
// 在此处对 `v` 做一些操作...
} // <- 此处 `v` 离开作用��,被释放,同时里面各项也被释放
借用检查器确保任何指针都不会悬挂,即只允许在Vector有效生命周期内合法读写其内容。
接下来,让我们继续学习下一种集合------字符串 String!