使用向量存储值列表
我们将首先研究的集合类型是Vec<T>
,也称为向量。
向量允许您在一个数据结构中存储多个值,这些值在内存中是相邻存储的。向量只能存储相同类型的值。当您有一系列项目时,例如文件中的文本行或购物车中商品的价格,向量非常有用。
创建新向量
要创建一个新的空向量,我们调用 Vec::new
函数,如清单 8-1 所示。
请注意,我们在这里添加了一个类型注解。因为我们没有向这个向量中插入任何值,Rust 不知道我们打算存储什么类型的元素。这是一个重要的点。向量是使用泛型实现的;我们将在第 10 章中介绍如何在你自己的类型中使用泛型。目前,要知道标准库提供的 Vec<T>
类型可以持有任何类型。当我们创建一个用于存储特定类型的向量时,我们可以在尖括号内指定类型。在清单 8-1 中,我们告诉 Rust Vec<T>
中的 v
将持有 i32
类型的元素。
更常见的是,您会用初始值创建一个Vec<T>
,而 Rust 会推断您想要存储的值的类型,因此您很少需要进行这种类型注解。Rust 方便地提供了 vec!
宏,它将创建一个持有您给定值的新向量。列表 8-2 创建了一个持有值 1
、2
和 3
的新 Vec<i32>
。整数类型是 i32
,因为这是默认的整数类型,正如我们在第 3 章的“数据类型” 部分所讨论的。
因为已经给出了初始 i32
值,Rust 可以推断出 v
的类型为 Vec<i32>
,因此类型注解不是必需的。接下来,我们将看看如何修改向量。
更新向量
要创建一个向量并添加元素,我们可以使用 push
方法,如清单 8-3 所示。
与任何变量一样,如果我们要能够更改其值,需要使用 mut
关键字使其可变,如第 3 章所述。我们放入的数字都是 i32
类型,Rust 会从数据中推断这一点,因此我们不需要 Vec<i32>
注解。
读取向量的元素
有两种方法可以引用存储在向量中的值:通过索引或使用 get
方法。在以下示例中,我们为这些函数返回的值的类型添加了注释,以增加清晰度。
列表 8-4 显示了访问向量中值的两种方法,使用索引语法和 get
方法。
注意这里的一些细节。我们使用索引值 2
来获取第三个元素,因为向量的索引从零开始。使用 &
和 []
可以让我们获得索引值处的元素的引用。当我们使用带有索引作为参数的 get
方法时,我们会得到一个 Option<&T>
,我们可以用 match
来处理。
Rust 提供了这两种引用元素的方法,因此你可以选择在尝试使用超出现有元素范围的索引值时程序的行为。例如,让我们看看当我们有一个包含五个元素的向量,然后尝试使用每种技术访问索引 100 处的元素时会发生什么,如清单 8-5 所示。
当我们运行这段代码时,第一个 []
方法会导致程序崩溃
因为它引用了一个不存在的元素。此方法最适合用于当尝试访问向量末尾之外的元素时
希望程序崩溃的情况。
当将一个超出向量范围的索引传递给 get
方法时,它会返回 None
而不会引发恐慌。如果在正常情况下偶尔可能会访问超出向量范围的元素,你会使用此方法。然后,你的代码将包含处理 Some(&element)
或 None
的逻辑,如第 6 章所述。例如,索引可能是由用户输入的数字。如果他们不小心输入了一个过大的数字,程序得到了一个 None
值,你可以告诉用户当前向量中有多少项,并给他们另一次机会输入一个有效的值。这比因为一个打字错误而使程序崩溃要更加用户友好!
当程序有一个有效的引用时,借用检查器会强制执行所有权和借用规则(在第4章中介绍),以确保此引用以及对向量内容的任何其他引用都保持有效。回想一下那条规则,即你不能在同一个作用域中同时拥有可变和不可变引用。这条规则适用于清单8-6,我们在其中持有一个向量中第一个元素的不可变引用,并尝试在向量末尾添加一个元素。如果我们稍后还试图引用该元素,这个程序将无法工作。
编译此代码将导致以下错误:
$ 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
值向量中每个元素的不可变引用并打印它们。
我们还可以迭代可变向量中每个元素的可变引用,以对所有元素进行修改。列表 8-8 中的 for
循环将向每个元素添加 50
。
要更改可变引用所指向的值,我们必须使用 *
解引用运算符来访问 i
中的值,然后才能使用 +=
运算符。我们将在第 15 章的 “使用解引用运算符跟踪指针到值” 部分详细讨论解引用运算符。
遍历向量,无论是不可变还是可变,都是安全的,因为借阅检查器的规则。如果我们试图在列表 8-7 和列表 8-8 的 for
循环体中插入或删除项,我们会得到一个类似于我们在列表 8-6 中代码所得到的编译器错误。for
循环持有的向量引用阻止了对整个向量的同时修改。
使用枚举存储多种类型
向量只能存储相同类型的值。这可能会不方便;确实存在需要存储不同类型的项目列表的使用场景。幸运的是,枚举的变体是在相同的枚举类型下定义的,因此当我们需要一种类型来表示不同类型的元素时,可以定义和使用枚举!
例如,假设我们想从电子表格中的一行获取值,其中该行的一些列包含整数,一些列包含浮点数,还有一些列包含字符串。我们可以定义一个枚举,其变体将持有不同的值类型,所有枚举变体都将被视为同一类型:枚举的类型。然后我们可以创建一个向量来保存该枚举,从而最终保存不同的类型。我们在列表 8-9 中演示了这一点。
Rust 需要知道在编译时向量中将包含什么类型,以便它确切地知道在堆上存储每个元素需要多少内存。我们还必须明确向量中允许的类型。如果 Rust 允许向量持有任何类型,那么其中一种或多种类型可能会导致对向量元素执行的操作出现错误。使用枚举加上 match
表达式意味着 Rust 将在编译时确保处理每种可能的情况,如第 6 章所述。
如果你不知道程序在运行时会得到哪些类型来存储在向量中,枚举技术就不起作用。相反,你可以使用特质对象,我们将在第 18 章中介绍。
现在我们已经讨论了一些使用向量的最常见方法,请务必查阅API 文档,了解标准库为 Vec<T>
定义的所有许多有用的方法。例如,除了 push
之外,pop
方法还会移除并返回最后一个元素。
释放一个向量会释放其元素
像任何其他 struct
一样,当向量超出作用域时,它会被释放,如清单 8-10 所注释。
当向量被丢弃时,其所有内容也会被丢弃,这意味着它持有的整数将被清理。借用检查器确保对向量内容的任何引用仅在向量本身有效时使用。
让我们继续下一个集合类型:String
!