使用 Vector 存储值列表
我们要看的第一个集合类型是 Vec<T>
,也称为 vector。Vector 允许您在单个数据结构中存储多个值,该结构将所有值在内存中彼此相邻放置。Vector 只能存储相同类型的值。当您有一个项目列表时,它们很有用,例如文件中的文本行或购物车中项目的价格。
创建新的 Vector
要创建一个新的、空的 vector,我们调用 Vec::new
函数,如列表 8-1 所示。
fn main() { let v: Vec<i32> = Vec::new(); }
列表 8-1:创建一个新的、空的 vector 来保存 i32
类型的值
请注意,我们在这里添加了类型注解。因为我们没有向这个 vector 中插入任何值,Rust 不知道我们打算存储哪种类型的元素。这是一个重要的点。Vector 是使用泛型实现的;我们将在第 10 章介绍如何将泛型用于您自己的类型。现在,请了解标准库提供的 Vec<T>
类型可以容纳任何类型。当我们创建一个 vector 来保存特定类型时,我们可以在尖括号内指定类型。在列表 8-1 中,我们告诉 Rust,v
中的 Vec<T>
将保存 i32
类型的元素。
通常,您会使用初始值创建一个 Vec<T>
,Rust 会推断您想要存储的值的类型,因此您很少需要进行这种类型注解。Rust 方便地提供了 vec!
宏,它将创建一个新的 vector,其中包含您给定的值。列表 8-2 创建一个新的 Vec<i32>
,其中包含值 1
、2
和 3
。整数类型是 i32
,因为这是默认的整数类型,正如我们在“数据类型”第 3 章的章节中讨论的那样。
fn main() { let v = vec![1, 2, 3]; }
列表 8-2:创建一个包含值的新 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 的元素
有两种方法可以引用存储在 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 中的项
请注意这里的一些细节。我们使用索引值 2
来获取第三个元素,因为 vector 是按数字索引的,从零开始。使用 &
和 []
为我们提供了对索引值处元素的引用。当我们使用 get
方法并将索引作为参数传递时,我们会得到一个 Option<&T>
,我们可以将其与 match
一起使用。
Rust 提供了这两种引用元素的方法,以便您可以选择当您尝试使用超出现有元素范围的索引值时程序如何表现。例如,让我们看看当我们有一个包含五个元素的 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:尝试访问包含五个元素的 vector 中索引 100 处的元素
当我们运行这段代码时,第一个 []
方法将导致程序 panic,因为它引用了一个不存在的元素。当您希望程序在尝试访问超出 vector 末尾的元素时崩溃时,最好使用此方法。
当 get
方法被传递一个超出 vector 范围的索引时,它会返回 None
而不会 panic。如果偶尔在正常情况下可能会发生访问超出 vector 范围的元素,您可以使用此方法。然后,您的代码将具有逻辑来处理 Some(&element)
或 None
,正如第 6 章中所讨论的那样。例如,索引可能来自某人输入的数字。如果他们不小心输入了一个太大的数字,并且程序得到一个 None
值,您可以告诉用户当前 vector 中有多少项,并给他们另一次输入有效值的机会。这比由于拼写错误而崩溃程序更用户友好!
当程序具有有效的引用时,借用检查器会强制执行所有权和借用规则(在第 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 末尾添加一个新元素可能需要分配新的内存并将旧元素复制到新空间,如果 vector 当前存储的位置没有足够的空间将所有元素彼此相邻地放置。在这种情况下,对第一个元素的引用将指向已释放的内存。借用规则阻止程序最终陷入这种情况。
注意:有关 Vec<T>
类型的实现细节的更多信息,请参阅 “The Rustonomicon”。
迭代 Vector 中的值
要依次访问 vector 中的每个元素,我们将遍历所有元素,而不是使用索引一次访问一个。列表 8-7 展示了如何使用 for
循环来获取对 i32
值 vector 中每个元素的不可变引用并打印它们。
fn main() { let v = vec![100, 32, 57]; for n_ref in &v { // n_ref has type &i32 let n_plus_one: i32 = *n_ref + 1; println!("{n_plus_one}"); } }
列表 8-7:通过使用 for
循环迭代元素来访问 vector 中的每个元素
要读取 n_ref
引用的数字,我们必须使用 *
解引用运算符来获取 n_ref
中的值,然后才能将其加 1,正如在 “解引用指针访问其数据” 中所介绍的那样。
我们还可以迭代可变 vector 中每个元素的可变引用,以便更改所有元素。列表 8-8 中的 for
循环将向每个元素添加 50
。
fn main() { let mut v = vec![100, 32, 57]; for n_ref in &mut v { // n_ref has type &mut i32 *n_ref += 50; } }
列表 8-8:迭代 vector 中元素的可变引用
要更改可变引用引用的值,我们再次使用 *
解引用运算符来获取 n_ref
中的值,然后才能使用 +=
运算符。
安全地使用迭代器
我们将在第 13.2 章 “使用迭代器处理一系列项” 中更详细地讨论迭代器的工作原理。现在,一个重要的细节是迭代器包含指向 vector 中数据的指针。我们可以通过将 for 循环反糖化为 Vec::iter
和 Iterator::next
的相应方法调用来了解迭代器的工作原理
请注意,迭代器 iter
是一个指针,它遍历 vector 的每个元素。next
方法推进迭代器并返回对前一个元素的可选引用,在 vector 末尾返回 Some
(我们解包)或 None
。
此细节与安全地使用 vector 相关。例如,假设我们想要就地复制一个 vector,例如 [1, 2]
变成 [1, 2, 1, 2]
。一个幼稚的实现可能看起来像这样,并使用编译器推断的权限进行注释
请注意,v.iter()
从 *v
中删除了 W 权限。因此,v.push(..)
操作缺少预期的 W 权限。Rust 编译器将拒绝此程序并显示相应的错误消息
error[E0502]: cannot borrow `*v` as mutable because it is also borrowed as immutable
--> test.rs:3:9
|
2 | for n_ref in v.iter() {
| --------
| |
| immutable borrow occurs here
| immutable borrow later used here
3 | v.push(*n_ref);
| ^^^^^^^^^^^^^^ mutable borrow occurs here
正如我们在第 4 章中讨论的那样,此错误背后的安全问题是读取已释放的内存。一旦 v.push(1)
发生,vector 将重新分配其内容并使迭代器的指针无效。因此,为了安全地使用迭代器,Rust 不允许您在迭代期间向 vector 添加或删除元素。
一种无需使用指针即可迭代 vector 的方法是使用范围,就像我们在 第 4.4 章 中用于字符串切片一样。例如,范围 0 .. v.len()
是对 vector v
的所有索引的迭代器,如下所示
使用枚举存储多种类型
Vector 只能存储相同类型的值。这可能很不方便;肯定有需要存储不同类型项目列表的用例。幸运的是,枚举的变体是在同一枚举类型下定义的,因此当我们需要一种类型来表示不同类型的元素时,我们可以定义和使用枚举!
例如,假设我们想从电子表格中的一行获取值,其中行中的某些列包含整数,一些浮点数和一些字符串。我们可以定义一个枚举,其变体将保存不同的值类型,并且所有枚举变体都将被视为相同的类型:枚举的类型。然后我们可以创建一个 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:定义一个 enum
以在一个 vector 中存储不同类型的值
Rust 需要在编译时知道 vector 中将包含哪些类型,以便它确切地知道在堆上存储每个元素需要多少内存。我们还必须明确允许在此 vector 中使用哪些类型。如果 Rust 允许 vector 容纳任何类型,则可能会有一种或多种类型会导致对 vector 元素执行的操作出现错误。使用枚举加上 match
表达式意味着 Rust 将在编译时确保处理每种可能的情况,正如第 6 章中所讨论的那样。
如果您不知道程序在运行时将获取哪些详尽的类型集以存储在 vector 中,则枚举技术将不起作用。相反,您可以使用 trait 对象,我们将在第 18 章中介绍。
现在我们已经讨论了一些使用 vector 的最常见方法,请务必查看 API 文档了解标准库在 Vec<T>
上定义的所有许多有用的方法。例如,除了 push
之外,pop
方法还会删除并返回最后一个元素。
丢弃 Vector 会丢弃其元素
像任何其他 struct
一样,当 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
!