使用向量存储值列表
我们将要研究的第一个集合类型是 Vec<T>
,也称为向量。向量允许您在单个数据结构中存储多个值,该数据结构将所有值在内存中彼此相邻。向量只能存储相同类型的值。当您有项目列表时,例如文件中的文本行或购物车中项目的价格,它们非常有用。
创建新向量
要创建一个新的空 vector,我们调用 Vec::new
函数,如图 8-1 所示。
fn main() { let v: Vec<i32> = Vec::new(); }
示例 8-1:创建一个新的空 vector 来保存 i32
类型的值
请注意,我们在此处添加了类型注释。因为我们没有向这个 vector 中插入任何值,所以 Rust 不知道我们打算存储什么样的元素。这是很重要的一点。向量是使用泛型实现的;我们将在第 10 章介绍如何将泛型与你自己的类型一起使用。现在,要知道标准库提供的 Vec<T>
类型可以容纳任何类型。当我们创建一个 vector 来保存特定类型时,我们可以在尖括号内指定类型。在示例 8-1 中,我们告诉 Rust v 中的
Vec<T>
将保存 i32
类型的元素。
更多时候,你会创建一个带有初始值的 Vec<T>
,而 Rust 会推断你想要存储的值的类型,所以你很少需要做这种类型注释。Rust 方便地提供了 vec!
宏,它将创建一个
new vector 来保存你给它的值。示例 8-2 创建了一个新的
Vec<i32>
,其中包含值 1
、2
和 3
。整数类型为 i32
因为这是默认的整数类型,正如我们在“数据
Types“部分。
fn main() { let v = vec![1, 2, 3]; }
示例 8-2:创建一个包含 values 的新 vector
因为我们给出了初始的 i32
值,所以 Rust 可以推断出 v
的类型
是 Vec<i32>
,并且类型注释不是必需的。接下来,我们将了解如何修改 vector。
更新 Vector
要创建一个 vector 然后向其添加元素,我们可以使用 push
方法,如示例 8-3 所示。
fn main() { 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 中的值:通过索引或使用 get
方法。在以下示例中,我们批注了从这些函数返回的值的类型,以便更加清晰。
示例 8-4 显示了访问 vector 中值的两种方法,包括索引语法和 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."), } }
示例 8-4:使用索引语法和使用 get
访问 vector 中项目的方法
请注意此处的一些详细信息。我们使用 index 值 2
来获取第三个元素,因为向量是按数字索引的,从 0 开始。使用 &
和 []
为我们提供了对 index 值处元素的引用。当我们使用 get
方法,并将索引作为参数传递,我们得到一个 Option<&T>
,我们可以将其与 match
一起使用。
Rust 提供了这两种引用元素的方法,因此你可以选择当你尝试使用现有元素范围之外的索引值时程序的行为方式。举个例子,让我们看看当我们有一个包含 5 个元素的 vector 时会发生什么,然后我们尝试使用每种技术访问索引为 100 的元素,如示例 8-5 所示。
fn main() { let v = vec![1, 2, 3, 4, 5]; let does_not_exist = &v[100]; let does_not_exist = v.get(100); }
示例 8-5:尝试访问包含 5 个元素的 vector 中索引为 100 的元素
当我们运行这段代码时,第一个 []
方法将导致程序 panic,因为它引用了一个不存在的元素。当你希望程序在尝试访问超过 vector 末尾的元素时崩溃时,最好使用此方法。
当 get
方法传递向量外部的索引时,它会返回
没有
一个没有恐慌。如果访问元素
超出向量范围时,正常情况下可能偶尔发生
情况 下。然后,您的代码将具有逻辑来处理
Some(&element)
或 None
,如第 6 章所述。例如,索引可能来自输入数字的人。如果他们不小心输入了一个太大的数字,并且程序获得了 None
值,您可以告诉用户当前向量中有多少个项目,并给他们另一个输入有效值的机会。这比由于拼写错误而使程序崩溃更用户友好!
当程序具有有效的引用时,借用检查器会强制执行所有权和借用规则(在第 4 章中介绍),以确保此引用和对 vector 内容的任何其他引用保持有效。回想一下规定不能在同一范围内拥有可变引用和不可变引用的规则。这条规则在示例 8-6 中适用,其中我们持有对 vector 中第一个元素的不可变引用,并尝试在末尾添加一个元素。如果我们稍后还尝试在函数中引用该元素,则此程序将不起作用。
fn main() {
let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0];
v.push(6);
println!("The first element is: {first}");
}
示例 8-6:尝试在持有对项目的引用时向 vector 添加元素
编译此代码将导致此错误:
$ 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 中的代码可能看起来应该可以工作:为什么对第一个元素的引用应该关心 vector 末尾的变化?此错误是由于 vector 的工作方式造成的:由于 vector 将值在内存中彼此相邻,因此在向量的末尾添加新元素可能需要分配新内存并将旧元素复制到新空间,如果没有足够的空间将所有元素放在当前存储 vector 的位置彼此相邻。在这种情况下,对第一个元素的引用将指向已释放的内存。借用规则可以防止程序最终出现这种情况。
注意:有关 Vec<T>
类型的实现详细信息的更多信息,请参阅“该
Rustonomicon”。
迭代 vector 中的值
要依次访问 vector 中的每个元素,我们将遍历所有元素,而不是使用索引一次访问一个元素。示例 8-7 展示了如何使用 for
循环来获取对 vector 中每个元素的不可变引用
i32
值并打印它们。
fn main() { let v = vec![100, 32, 57]; for i in &v { println!("{i}"); } }
示例 8-7:通过使用 for
循环迭代元素来打印 vector 中的每个元素
我们还可以迭代对可变 vector 中每个元素的可变引用,以便对所有元素进行更改。示例 8-8 中的 for
循环将为每个元素添加 50
。
fn main() { let mut v = vec![100, 32, 57]; for i in &mut v { *i += 50; } }
示例 8-8:迭代对 vector 中元素的可变引用
要更改可变引用引用的值,我们必须使用
*
dereference 运算符获取 i
中的值,然后我们才能使用 +=
算子。我们将在“在
指向带有 Dereference 运算符的 Value 的指针”
第 15 章的章节。
由于 borrow checker 的规则,迭代 vector,无论是不可变的还是可变的,都是安全的。如果我们尝试在 for
中插入或移除
循环体在示例 8-7 和示例 8-8 中,我们会得到一个编译器错误
类似于示例 8-6 中的代码。对
for
循环保存的 vector 可以防止同时修改整个 vector。
使用枚举存储多种类型
向量只能存储相同类型的值。这可能会带来不便;肯定有需要存储不同类型项目列表的用例。幸运的是,枚举的变体是在相同的枚举类型下定义的,因此当我们需要一种类型来表示不同类型的元素时,我们可以定义并使用枚举!
例如,假设我们想要从电子表格中的一行中获取值,其中行中的一些列包含整数、一些浮点数和一些字符串。我们可以定义一个枚举,其 variations 将包含不同的值类型,并且所有 enum 变体都将被视为相同的类型:枚举的类型。然后我们可以创建一个 vector 来保存该枚举,从而最终保存不同的类型。我们已经在示例 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), ]; }
示例 8-9:定义一个枚举
以将不同类型的值存储在一个 vector 中
Rust 需要知道编译时 vector 中将有哪些类型,以便它确切地知道堆上需要多少内存来存储每个元素。我们还必须明确这个 vector 中允许哪些类型。如果 Rust 允许一个 vector 保存任何类型,那么一个或多个类型就有可能导致对 vector 的元素执行的作出错。使用枚举加上 match
表达式意味着 Rust 将确保在编译时处理所有可能的情况,如第 6 章所述。
如果你不知道程序在运行时将获取多少种类型来存储在 vector 中,那么 enum 技术将不起作用。相反,你可以使用 trait 对象,我们将在第 17 章中介绍。
现在我们已经讨论了一些最常用的向量使用方法,请务必查看 API 文档,了解标准库在 Vec<T>
上定义的所有有用方法。例如,除了 push
之外,pop
方法还会删除并返回最后一个元素。
删除 Vector 会删除其元素
与任何其他结构
体一样,当 vector 超出范围时,它会被释放,如示例 8-10 所示。
fn main() { { let v = vec![1, 2, 3, 4]; // do stuff with v } // <- v goes out of scope and is freed here }
示例 8-10:显示 vector 及其元素的放置位置
当 vector 被删除时,它的所有内容也会被删除,这意味着它保存的整数将被清理。借用检查器确保仅在 vector 本身有效时使用对 vector 内容的任何引用。
让我们继续下一个集合类型:String
!