使用向量存储值列表
我们要介绍的第一种集合类型是 Vec<T>,也称为向量。向量可以让你在一个数据结构中存储多个值,并将这些值连续地放在内存中。向量只能存储相同类型的值。当你需要保存一个条目列表时,向量就很有用,例如文件中的文本行或购物车中各商品的价格。
创建新向量
要创建一个新的空向量,我们可以调用 Vec::new 函数,如示例 8-1 所示。
fn main() {
let v: Vec<i32> = Vec::new();
}
i32 类型的值请注意,我们在这里添加了类型注解。由于尚未向这个向量插入任何值,Rust 不知道我们打算存储哪种元素。这一点很重要。向量使用泛型实现;我们会在第 10 章介绍如何在自己的类型中使用泛型。目前只需知道,标准库提供的 Vec<T> 类型可以存储任意类型。创建用于存储特定类型的向量时,我们可以在尖括号中指定类型。在示例 8-1 中,我们告诉 Rust,v 中的 Vec<T> 将存储 i32 类型的元素。
更常见的做法是使用初始值创建 Vec<T>,Rust 会推断你想要存储的值的类型,因此通常不需要添加类型注解。Rust 提供了方便的 vec! 宏,可以创建一个包含所给值的新向量。示例 8-2 创建了一个新的 Vec<i32>,其中包含值 1、2 和 3。整数类型是 i32,因为它是默认整数类型,正如我们在第 3 章“数据类型”一节中讨论的那样。
fn main() {
let v = vec![1, 2, 3];
}
由于我们提供了初始 i32 值,Rust 可以推断 v 的类型是 Vec<i32>,因此不需要类型注解。接下来,我们来看看如何修改向量。
更新向量
要创建向量并向其中添加元素,我们可以使用 push 方法,如示例 8-3 所示。
fn main() {
let mut v = Vec::new();
v.push(5);
v.push(6);
v.push(7);
v.push(8);
}
push 方法向向量添加值和其他变量一样,如果我们想要能够修改它的值,就需要使用 mut 关键字将其声明为可变变量,这一点已在第 3 章讨论过。我们放入其中的数字都是 i32 类型,Rust 会根据数据推断出这一点,因此不需要标注 Vec<i32>。
读取向量元素
有两种方式可以引用向量中存储的值:通过索引,或使用 get 方法。为了更加清晰,下面的示例标注了这些函数返回值的类型。
示例 8-4 展示了通过索引语法和 get 方法访问向量中值的两种方式。
fn main() {
let v = vec![1, 2, 3, 4, 5];
let third: &i32 = &v[2];
println!("The third element is {third}");
let third: Option<&i32> = v.get(2);
match third {
Some(third) => println!("The third element is {third}"),
None => println!("There is no third element."),
}
}
get 方法访问向量中的项这里有几点需要注意。由于向量的索引从 0 开始,因此我们使用索引值 2 来获取第三个元素。使用 & 和 [] 会得到对该索引位置元素的引用。使用 get 方法并将索引作为参数传入时,我们会得到一个可以与 match 一起使用的 Option<&T>。
Rust 提供了这两种引用元素的方式,让你可以选择当尝试使用超出已有元素范围的索引时,程序应如何处理。举例来说,如示例 8-5 所示,假设有一个包含五个元素的向量,我们分别使用这两种方式访问索引 100 处的元素,看看会发生什么。
fn main() {
let v = vec![1, 2, 3, 4, 5];
let does_not_exist = &v[100];
let does_not_exist = v.get(100);
}
运行这段代码时,第一个 [] 方法会导致程序 panic,因为它引用了一个不存在的元素。当你希望程序在有人尝试访问向量末尾之后的元素时崩溃,这种方式最合适。
如果传给 get 方法的索引超出了向量范围,它会返回 None,而不会触发 panic。如果在正常情况下偶尔可能访问超出向量范围的元素,就可以使用这种方法。此时,代码可以像第 6 章讨论的那样,处理得到 Some(&element) 或 None 的情况。例如,索引可能来自用户输入。如果用户不小心输入了过大的数字,导致程序得到 None,你可以告知用户当前向量中有多少项,并让他们再次输入有效值。与因输入错误而让程序崩溃相比,这样对用户更友好!
当程序持有有效引用时,借用检查器会强制执行所有权和借用规则(第 4 章介绍过),以确保该引用以及指向向量内容的其他引用始终有效。回顾一下这条规则:在同一作用域中,不能同时拥有可变引用和不可变引用。示例 8-6 就涉及这条规则:我们持有向量第一个元素的不可变引用,同时尝试在末尾添加一个元素。如果之后还要在函数中引用该元素,这段程序就无法运行。
fn main() {
let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0];
v.push(6);
println!("The first element is: {first}");
}
编译这段代码会产生以下错误:
$ cargo run
Compiling collections v0.1.0 (file:///projects/collections)
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
For more information about this error, try `rustc --explain E0502`.
error: could not compile `collections` (bin "collections") due to 1 previous error
示例 8-6 中的代码看起来似乎应该可以运行:为什么对第一个元素的引用会受向量末尾变化的影响?这是由向量的工作方式导致的:由于向量会将值连续存放在内存中,如果当前位置没有足够空间容纳所有元素,那么在向量末尾添加新元素时可能需要分配新的内存,并将旧元素复制到新位置。在这种情况下,指向第一个元素的引用就会指向已释放的内存。借用规则可以防止程序陷入这种状态。
注意:关于 Vec<T> 类型实现细节的更多信息,请参阅《Rustonomicon》。
遍历向量中的值
要依次访问向量中的每个元素,我们可以遍历所有元素,而不是每次用索引访问一个元素。示例 8-7 展示了如何使用 for 循环获取 i32 向量中每个元素的不可变引用并打印它们。
fn main() {
let v = vec![100, 32, 57];
for i in &v {
println!("{i}");
}
}
for 循环遍历向量中的元素并逐个打印我们也可以遍历可变向量中每个元素的可变引用,从而修改所有元素。示例 8-8 中的 for 循环会给每个元素加上 50。
fn main() {
let mut v = vec![100, 32, 57];
for i in &mut v {
*i += 50;
}
}
要修改可变引用所指向的值,我们必须先使用 * 解引用运算符取得 i 中的值,然后才能使用 += 运算符。第 15 章的“通过解引用运算符访问指针指向的值”一节会进一步介绍解引用运算符。
无论以不可变还是可变方式遍历向量,由于借用检查器的规则,这样做都是安全的。如果我们尝试在示例 8-7 和示例 8-8 的 for 循环体中插入或移除元素,就会得到与示例 8-6 中类似的编译错误。for 循环持有的向量引用会阻止同时修改整个向量。
使用枚举存储多种类型
向量只能存储相同类型的值。这可能不太方便,因为确实存在需要存储不同类型条目列表的情况。幸运的是,枚举的变体都属于同一个枚举类型,因此当我们需要用一种类型表示不同类型的元素时,可以定义并使用枚举!
例如,假设我们想要获取电子表格某一行中的值,其中有些列包含整数,有些包含浮点数,还有些包含字符串。我们可以定义一个枚举,让它的变体分别存储不同类型的值;所有枚举变体都属于同一种类型,也就是该枚举的类型。然后,我们就可以创建一个存储该枚举的向量,从而存储不同类型的值。示例 8-9 展示了这种做法。
fn main() {
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}
let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];
}
Rust 需要在编译时知道向量中会存储哪些类型,这样才能准确确定在堆上存储每个元素所需的内存量。我们还必须明确指定这个向量允许存储哪些类型。如果 Rust 允许向量存储任意类型,那么对向量元素执行的操作可能会导致其中一种或多种类型出错。像第 6 章讨论的那样,使用枚举和 match 表达式,意味着 Rust 会在编译时确保处理了所有可能的情况。
如果你在运行时才知道程序可能会在向量中存储哪些类型,并且无法穷举这些类型,那么枚举技巧就不可行。此时可以使用 trait 对象,我们会在第 18 章介绍它。
现在我们已经讨论了几种最常见的向量用法,请务必查阅 Vec<T> 的API 文档,了解标准库为它定义的许多实用方法。例如,除了 push 之外,pop 方法还可以移除并返回最后一个元素。
丢弃向量时也会丢弃其元素
和其他 struct 一样,向量离开作用域时也会被释放,如示例 8-10 所示。
fn main() {
{
let v = vec![1, 2, 3, 4];
// do stuff with v
} // <- v goes out of scope and is freed here
}
向量被丢弃时,其中的所有内容也会被丢弃,这意味着它所持有的整数会被清理。借用检查器会确保只有在向量本身有效期间,才能使用指向其内容的引用。
接下来我们继续介绍下一种集合类型:String!