8.1 用向量存储值的列表
7 分钟阅读
译文 · 基于 The Rust Programming Language(rustc 1.97.1)
用向量存储值的列表
原文链接: https://doc.rust-lang.org/stable/book/ch08-01-vectors.html
用向量存储值的列表
我们要看的第一种集合类型是 Vec<T>,也称为向量(vector)。向量让你能把多个值存放在同一个数据结构中,并在内存里彼此相邻。向量只能存放相同类型的值。当你有一份列表时——例如文件中的文本行,或购物车里商品的价格——向量就很有用。
创建新向量
要创建一个新的空向量,我们调用 Vec::new 函数,如示例 8-1 所示。
| |
示例 8-1:创建一个用来存放 i32 类型值的新空向量
注意这里我们加了类型注解。因为还没有向这个向量插入任何值,Rust 不知道我们打算存放哪种元素。这一点很重要。向量是用泛型实现的;我们将在第 10 章介绍如何在自己的类型上使用泛型。现在只需知道,标准库提供的 Vec<T> 类型可以存放任意类型。创建用于存放特定类型的向量时,可以在尖括号中指定类型。在示例 8-1 中,我们告诉 Rust:v 中的 Vec<T> 将存放 i32 类型的元素。
更常见的情况是,你会带着初始值创建 Vec<T>,Rust 会推断你想存放的值的类型,因此很少需要写这种类型注解。Rust 还方便地提供了 vec! 宏,它会创建一个包含你所给值的新向量。示例 8-2 创建了一个包含值 1、2 和 3 的新 Vec<i32>。整数类型是 i32,因为那是默认的整数类型,正如我们在第 3 章「数据类型」一节中讨论的。
| |
示例 8-2:创建一个包含值的新向量
因为我们给出了初始的 i32 值,Rust 可以推断出 v 的类型是 Vec<i32>,因此不需要类型注解。接下来看看如何修改向量。
更新向量
要创建向量并向其中添加元素,可以使用 push 方法,如示例 8-3 所示。
| |
示例 8-3:用 push 方法向向量添加值
与任何变量一样,若希望能够改变它的值,就需要用 mut 关键字使其可变,这在第 3 章讨论过。我们放入的数字都是 i32 类型,Rust 会从数据中推断出来,因此不需要 Vec<i32> 注解。
读取向量中的元素
引用向量中存储的值有两种方式:通过索引,或使用 get 方法。在下面的例子中,我们为这些函数返回值的类型加了注解,以便更清晰。
示例 8-4 展示了访问向量中某个值的两种方法:索引语法和 get 方法。
| |
示例 8-4:使用索引语法和 get 方法访问向量中的项
这里有几处细节需要注意。我们用索引值 2 获取第三个元素,因为向量按数字索引,且从零开始。使用 & 和 [] 会得到该索引处元素的引用。当我们把索引作为参数传给 get 方法时,会得到一个可以配合 match 使用的 Option<&T>。
Rust 提供这两种引用元素的方式,是为了让你可以选择:当试图使用超出已有元素范围的索引时,程序应如何表现。例如,假设我们有一个包含五个元素的向量,然后分别用这两种技巧访问索引 100 处的元素,如示例 8-5 所示。
| |
示例 8-5:尝试访问含五个元素的向量中索引为 100 的元素
运行这段代码时,第一种 [] 方法会使程序 panic,因为它引用了不存在的元素。若你希望一旦有人尝试访问向量末尾之后的元素程序就崩溃,用这种方法最合适。
当传给 get 方法的索引超出向量范围时,它会返回 None 而不会 panic。若在正常情况下偶尔可能访问到向量范围之外的元素,就适合用这种方法。然后你的代码需要有处理 Some(&element) 或 None 的逻辑,正如第 6 章所讨论的。例如,索引可能来自用户输入的数字。若他们不小心输入了过大的数字,程序得到 None,你就可以告诉用户当前向量中有多少项,并再给他们一次输入有效值的机会。这比因用户一次输入错误就让程序崩溃要友好得多!
当程序持有有效引用时,借用检查器会强制执行所有权与借用规则(第 4 章讲过),以确保该引用以及指向向量内容的任何其他引用都保持有效。回想那条规则:不能在同一作用域中同时拥有可变引用和不可变引用。示例 8-6 就适用这条规则:我们持有向量第一个元素的不可变引用,又试图在末尾添加一个元素。若我们还想在函数后面引用那个元素,这个程序就无法工作。
| |
示例 8-6:在持有某项引用的同时尝试向向量添加元素
编译这段代码会得到如下错误:
| |
示例 8-6 中的代码看起来似乎应该能工作:为什么对第一个元素的引用要关心向量末尾的改动?这个错误源于向量的工作方式:因为向量把值彼此相邻地放在内存中,若当前位置没有足够空间让所有元素继续相邻存放,在末尾添加新元素可能需要分配新内存,并把旧元素复制到新空间。那样的话,指向第一个元素的引用就会指向已释放的内存。借用规则阻止程序陷入这种局面。
注意:关于
Vec<T>类型实现细节的更多内容,请参阅《The Rustonomicon》。
遍历向量中的值
要依次访问向量中的每个元素,我们会遍历所有元素,而不是用索引一次取一个。示例 8-7 展示了如何用 for 循环获取 i32 值向量中每个元素的不可变引用并打印它们。
| |
示例 8-7:用 for 循环遍历元素并打印向量中的每一项
我们也可以遍历可变向量中每个元素的可变引用,以便修改所有元素。示例 8-8 中的 for 循环会给每个元素加上 50。
| |
示例 8-8:遍历向量中元素的可变引用
要改变可变引用所指向的值,必须先用 * 解引用运算符得到 i 中的值,然后才能使用 += 运算符。我们会在第 15 章「跟随引用到达值」一节中更多地讨论解引用运算符。
无论是不可变还是可变地遍历向量,在借用检查器规则下都是安全的。若我们试图在示例 8-7 和示例 8-8 的 for 循环体中插入或删除项,就会得到与示例 8-6 代码类似的编译错误。for 循环所持有的对向量的引用,会阻止同时修改整个向量。
用枚举来存储多种类型
向量只能存放相同类型的值。这有时会不方便;确实存在需要存储不同类型项列表的用例。幸好,枚举的各个变体都定义在同一个枚举类型之下,因此当我们需要用一种类型表示不同类型的元素时,可以定义并使用枚举!
例如,假设我们要从电子表格的一行中取值,其中有些列是整数,有些是浮点数,有些是字符串。我们可以定义一个枚举,其变体分别持有不同的值类型,而所有枚举变体都被视为同一类型:即该枚举的类型。然后可以创建一个存放该枚举的向量,从而最终存放不同的类型。我们在示例 8-9 中演示了这一点。
| |
示例 8-9:定义枚举以便在一个向量中存储不同类型的值
Rust 需要在编译期知道向量中会有哪些类型,这样才能确切知道堆上存储每个元素需要多少内存。我们也必须明确指出这个向量允许哪些类型。若 Rust 允许向量存放任意类型,就可能有一种或多种类型会在对向量元素执行操作时引发错误。使用枚举加上 match 表达式,意味着 Rust 会在编译期确保每个可能的情况都被处理,正如第 6 章所讨论的。
若你在运行时才知道程序会把哪些完整类型集合存进向量,枚举技巧就不适用了。这时可以使用特征对象(trait object),我们会在第 18 章介绍。
既然我们已经讨论了使用向量的一些最常见方式,请务必查阅API 文档,了解标准库在 Vec<T> 上定义的众多有用方法。例如,除了 push,还有 pop 方法可以移除并返回最后一个元素。
丢弃向量也会丢弃其元素
与任何其他 struct 一样,向量在离开作用域时会被释放,如示例 8-10 中的标注所示。
| |
示例 8-10:展示向量及其元素被丢弃的位置
当向量被丢弃时,其全部内容也会被丢弃,也就是说它所容纳的整数会被清理干净。借用检查器确保:对向量内容的任何引用,都只在向量本身仍然有效时才被使用。
让我们进入下一种集合类型:String!