简介
欢迎来到「使用向量存储值列表」实验。本实验是 《Rust 程序设计语言》 的一部分。你可以在 LabEx 中练习 Rust 技能。
「在本实验中,我们将探索 Vec<T>
集合类型,也称为向量,它允许在单个数据结构中存储相同类型的值列表。」
This tutorial is from open-source community. Access the source code
💡 本教程由 AI 辅助翻译自英文原版。如需查看原文,您可以 切换至英文原版
欢迎来到「使用向量存储值列表」实验。本实验是 《Rust 程序设计语言》 的一部分。你可以在 LabEx 中练习 Rust 技能。
「在本实验中,我们将探索 Vec<T>
集合类型,也称为向量,它允许在单个数据结构中存储相同类型的值列表。」
我们要介绍的第一种集合类型是 Vec<T>
,也称为 向量。向量允许你在单个数据结构中存储多个值,这些值在内存中彼此相邻存放。向量只能存储相同类型的值。当你有一系列项目时,比如文件中的文本行或购物车中商品的价格,向量就很有用。
要创建一个新的空向量,我们调用 Vec::new
函数,如清单 8-1 所示。
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 创建了一个新的 Vec<i32>
,它包含值 1
、2
和 3
。整数类型是 i32
,因为这是我们在「数据类型」中讨论过的默认整数类型。
let v = vec![1, 2, 3];
清单 8-2:创建一个包含值的新向量
因为我们给出了初始的 i32
值,Rust 可以推断出 v
的类型是 Vec<i32>
,所以类型标注不是必需的。接下来,我们将看看如何修改向量。
要创建一个向量并向其中添加元素,我们可以使用 push
方法,如清单 8-3 所示。
let mut v = Vec::new();
v.push(5);
v.push(6);
v.push(7);
v.push(8);
清单 8-3:使用 push
方法向向量中添加值
和任何变量一样,如果我们想要能够改变它的值,就需要使用 mut
关键字使其可变,正如第 3 章所讨论的那样。我们放入的数字都是 i32
类型,Rust 会从数据中推断出这一点,所以我们不需要 Vec<i32>
标注。
有两种方法可以引用存储在向量中的值:通过索引或使用 get
方法。在以下示例中,为了更清晰,我们标注了这些函数返回值的类型。
清单 8-4 展示了使用索引语法和 get
方法这两种访问向量中值的方式。
let v = vec![1, 2, 3, 4, 5];
1 let third: &i32 = &v[2];
println!("The third element is {third}");
2 let third: Option<&i32> = v.get(2);
match third {
Some(third) => println!("The third element is {third}"),
None => println!("There is no third element."),
}
清单 8-4:使用索引语法和 get
方法访问向量中的元素
这里需要注意一些细节。我们使用索引值 2
来获取第三个元素 [1],因为向量是从 0 开始编号索引的。使用 &
和 []
可以获取索引值处元素的引用。当我们将索引作为参数传递给 get
方法时 [2],会得到一个 Option<&T>
,我们可以使用 match
来处理它。
Rust 提供这两种引用元素的方式,以便你可以选择当尝试使用超出现有元素范围的索引值时程序的行为。例如,让我们看看当我们有一个包含五个元素的向量,然后尝试使用每种技术访问索引为 100 的元素时会发生什么,如清单 8-5 所示。
let v = vec![1, 2, 3, 4, 5];
let does_not_exist = &v[100];
let does_not_exist = v.get(100);
清单 8-5:尝试访问包含五个元素的向量中索引为 100 的元素
当我们运行这段代码时,第一个使用 []
方法的操作会导致程序恐慌,因为它引用了一个不存在的元素。当试图访问超出向量末尾的元素时,如果希望程序崩溃,这种方法是最合适的。
当 get
方法接收到一个超出向量范围的索引时,它会返回 None
而不会恐慌。如果在正常情况下偶尔会发生访问超出向量范围的元素的情况,你可以使用这种方法。然后你的代码可以有逻辑来处理 Some(&element)
或 None
的情况,正如第 6 章所讨论的。例如,索引可能来自用户输入的数字。如果他们不小心输入了一个太大的数字,程序得到一个 None
值,你可以告诉用户当前向量中有多少个元素,并给他们另一次机会输入一个有效的值。这比因为一个拼写错误而导致程序崩溃要更友好!
当程序有一个有效的引用时,借用检查器会强制执行所有权和借用规则(在第 4 章介绍),以确保这个引用以及任何其他对向量内容的引用保持有效。回想一下那个规则:在同一作用域内不能同时有可变引用和不可变引用。这个规则在清单 8-6 中适用,在那里我们对向量中的第一个元素持有一个不可变引用,然后试图在末尾添加一个元素。如果我们在函数后面还试图引用那个元素,这个程序将无法工作。
let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0];
v.push(6);
println!("The first element is: {first}");
清单 8-6:在持有对一个元素的引用时尝试向向量中添加元素
编译这段代码会导致如下错误:
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
清单 8-6 中的代码看起来似乎应该能工作:为什么对第一个元素的引用会关心向量末尾的变化呢?这个错误是由于向量的工作方式导致的:因为向量在内存中是将值相邻存放的,如果没有足够的空间将所有元素相邻存放在向量当前存储的位置,那么在向量末尾添加一个新元素可能需要分配新的内存并将旧元素复制到新空间。在这种情况下,对第一个元素的引用可能会指向已释放的内存。借用规则可以防止程序陷入这种情况。
注意:有关
Vec<T>
类型的更多实现细节,请参阅 https://doc.rust-lang.org/nomicon/vec/vec.html 上的「The Rustonomicon」。
要依次访问向量中的每个元素,我们可以遍历所有元素,而不是使用索引逐个访问。清单 8-7 展示了如何使用 for
循环来获取 i32
值的向量中每个元素的不可变引用并打印它们。
let v = vec![100, 32, 57];
for i in &v {
println!("{i}");
}
清单 8-7:通过使用 for
循环遍历元素来打印向量中的每个元素
我们也可以遍历可变向量中每个元素的可变引用,以便对所有元素进行修改。清单 8-8 中的 for
循环会给每个元素加上 50
。
let mut v = vec![100, 32, 57];
for i in &mut v {
*i += 50;
}
清单 8-8:遍历向量中元素的可变引用
为了更改可变引用所指向的值,我们必须使用 *
解引用运算符来获取 i
中的值,然后才能使用 +=
运算符。我们将在「跟随指针获取值」中更多地讨论解引用运算符。
由于借用检查器的规则,无论以不可变还是可变方式遍历向量都是安全的。如果我们试图在清单 8-7 和清单 8-8 的 for
循环体中插入或删除项目,我们会得到一个类似于清单 8-6 中的代码所产生的编译器错误。for
循环持有的对向量的引用会阻止对整个向量的同时修改。
向量只能存储相同类型的值。这可能会带来不便;在某些用例中,确实需要存储不同类型的项的列表。幸运的是,枚举的变体是在同一个枚举类型下定义的,所以当我们需要一种类型来表示不同类型的元素时,可以定义并使用枚举!
例如,假设我们要从电子表格的一行中获取值,其中该行的某些列包含整数,某些列包含浮点数,还有一些列包含字符串。我们可以定义一个枚举,其变体将持有不同的值类型,并且所有枚举变体都将被视为相同的类型:即枚举的类型。然后我们可以创建一个向量来持有该枚举,从而最终持有不同的类型。我们在清单 8-9 中演示了这一点。
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}
let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];
清单 8-9:定义一个枚举以在一个向量中存储不同类型的值
Rust 需要在编译时知道向量中将会有哪些类型,以便确切知道在堆上需要多少内存来存储每个元素。我们还必须明确这个向量中允许哪些类型。如果 Rust 允许向量持有任何类型,那么可能会有一个或多个类型在对向量元素执行操作时导致错误。正如第 6 章所讨论的,使用枚举加 match
表达式意味着 Rust 将在编译时确保处理每一种可能的情况。
如果你不知道程序在运行时要存储在向量中的所有可能类型,枚举技术就不起作用。相反,你可以使用特征对象,我们将在第 17 章介绍。
既然我们已经讨论了一些使用向量的最常见方法,一定要查看标准库为 Vec<T>
定义的所有许多有用方法的 API 文档。例如,除了 push
方法外,pop
方法会移除并返回最后一个元素。
和任何其他 struct
一样,当向量超出作用域时会被释放,如清单 8-10 所示。
{
let v = vec![1, 2, 3, 4];
// 对 v 进行一些操作
} // <- v 超出作用域并在此处被释放
清单 8-10:展示向量及其元素被释放的位置
当向量被释放时,它的所有内容也会被释放,这意味着它所包含的整数将被清理。借用检查器确保对向量内容的任何引用仅在向量本身有效时使用。
让我们继续学习下一种集合类型:String
!
恭喜你!你已经完成了「使用向量存储值列表」实验。你可以在 LabEx 中练习更多实验来提升你的技能。