使用矢量值存储值的清单
Storing Lists of Values with Vectors
我们将介绍的首个集合类型为 Vec<T>
,也称为 矢量。矢量值允许咱们将多个值,存储在会将全部这些值,依次存放于内存里的单个数据结构中。矢量只能存储相同类型的值 1。当咱们需要存储某个项目清单,例如某个文件中的文本行或购物车中的商品价格时,矢量值就非常有用。
译注:与后面的 哈希图 一样,矢量值也是同构的。
创建一个新矢量值
要创建一个新的空矢量值,我们要调用 Vec::new
函数,如下清单 8-1 所示。
#![allow(unused)] fn main() { 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 就会创建一个存储了值 1
、2
及 3
的新 Vec<i32>
。其中整数类型为 i32
,因为这是默认的整数类型,正如我们在第 3 章 “数据类型” 小节所讨论的。
#![allow(unused)] fn main() { let v = vec! [1, 2, 3]; }
清单 8-2:创建一个包含了一些值的新矢量
由于我们已给出了那些初始 i32
值,Rust 便可推断出 v
的类型为 Vec<i32>
,因此类型注解就不必要了。接下来,我们将探讨如何修改某个矢量。
更新矢量值
要创建某个矢量并随后向其添加元素,我们可使用 push
方法,如下清单 8-3 所示。
#![allow(unused)] fn main() { let mut v = Vec::new(); v.push(5); v.push(6); v.push(7); v.push(8); }
清单 8-3:使用 push
方法把一些值添加到某个矢量
正如在第 3 章中曾讨论过的,与任何变量一样,在我们想要能修改其值时,我们就需要使用 mut
关键字令其成为可变的。我们放入其中的数字均为 i32
类型,而 Rust 会从该数据推断出这点,因此我们无需添加 Vec<i32>
的注解。
读取矢量的元素
Reading Elements of Vectors
引用存储在矢量中某个值的方式有两种:
- 经由索引;
- 或使用
get
方法。
在下面的示例中,我们已注解了这些函数所返回值的类型,以增加辨识度。
下面列表 8-4 展示了访问矢量中某个值的两种方法,即通过索引语法与使用 get
方法。
#![allow(unused)] fn main() { let v = vec! [1, 2, 3, 4]; let third: &i32 = &v[2]; println! ("第三个元素为 {}", third); match v.get(2) { Some(third) => println! ("第三个元素为 {}", third), None => println! ("没有第三个元素。"), } }
清单 8-4:使用索引语法与 get
方法,访问矢量中的某个项目
译注:与
get
一样,[]
这个运算符同样是个方法。
请留意以下几点。我们使用索引值 2
获取第三个元素,因为矢量是以数字索引的,从零开始。使用 &
及 []
给到我们到索引值处元素的引用。当我们以作为参数传递的索引,使用 get
方法时,我们会得到一个可与 match
使用的 Option<&T>
。
Rust 提供了这两种引用某个元素的方式,这样在尝试使用超出现有元素范围的索引值时,咱们就可以选择程序的行事方式。例如,我们来看看在我们有个五元素的矢量,然后尝试使用两种方式分别访问索引为 100
的元素时会发生什么,如下清单 8-5 所示。
#![allow(unused)] 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:在包含五个元素的某个矢量中,尝试访问索引 100
处的元素
当我们运行这段代码时,由于其中第一个 []
方法引用了某个不存在的元素,因此其将造成该程序死机。在咱们希望咱们的程序,在尝试访问矢量末尾之后某个元素时要崩溃的情况下,就最好使用这个方法。
而当传递给 get
方法的是个超出该矢量范围的索引时,他会在不死机下返回 None
。在正常情况偶尔需要访问矢量范围之外的某个元素时,咱们就要使用这个方法。此时咱们的代码,将有着处理 Some(&element)
或 None
情形代码,正如曾在第 6 章中讨论过的。例如,索引可能来自用户输入的数字。在用户不小心输入了某个过大的数字,程序得到一个 None
值时,咱们就可以告知用户,当前矢量中有多少元素,并给他们另一次输入有效值的机会。这比因输入错误而导致程序崩溃要用户友好得多!
当程序有了某个有效引用时,借用检查器会强制执行所有权及借用规则检查(第 4 章中讲到的),确保该引用以及到矢量内容的任何其他引用保持有效。回顾一下那条规则:在同一作用域内,咱们不能同时有着可变和不可变引用。这条规则适用于下面示例 8-6,其中我们有个到某矢量中首个元素的不可变引用,并尝试添加一个元素到其末尾。若我们在函数的稍后还尝试引用该元素,那么该程序将不会工作。
#![allow(unused)] fn main() { let mut v = vec! [1, 2, 3, 4, 5]; let first: &i32 = &v[0]; v.push(6); println! ("首个元素为:{}", first); }
清单 8-6:在保留到矢量某个条目的引用同时,尝试将一个元素添加到该矢量
编译这段代码将得到如下报错:
$ cargo run
Compiling vec_demo v0.1.0 (/home/peng/rust-lang/projects/vec_demo)
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
--> src/main.rs:6:5
|
4 | let first: &i32 = &v[0];
| - immutable borrow occurs here
5 |
6 | v.push(6);
| ^^^^^^^^^ mutable borrow occurs here
7 |
8 | println! ("首个元素为:{}", first);
| ----- immutable borrow later used here
For more information about this error, try `rustc --explain E0502`.
error: could not compile `vec_demo` due to previous error
清单 8-6 中的代码看起来似乎应该工作:为什么到首个元素的引用,会关心该矢量末尾的更改?这个报错是由于矢量的工作方式造成的:因为矢量会把值连续地存储在内存中,而向矢量末尾添加新元素就可能需要分配新的内存,在该矢量当前存储的位置没有连续存放全部元素的足够空间时,那些原有元素就会被拷贝到新的内存空间。在这种情况下,到首个元素的引用,就会指向已释放内存。借用规则阻止了程序陷入这种情况。
请注意:更多有关
Vec<T>
类型的实现细节,请参考 Rust 专论,The Rustonomicon。
迭代矢量中的值
Iterating over the Values in a Vector
要依次访问矢量中的每个元素,我们就要遍历所有元素,而不是使用索引一次访问一个元素。下面清单 8-7 展示了如何使用 for
循环,获取到某个 i32
值矢量中,各个元素的不可变引用并打印出来。
#![allow(unused)] fn main() { let v = vec! [100, 32, 57]; for i in &v { println! ("{}", i); } }
清单 8-7:使用 for
循迭代元素,打印某矢量中的各个元素
我们还可以遍历到可变矢量中,各个元素的可变引用,对所有元素进行更改。下面清单 8-8 中的 for
循环,将把 50
加到每个元素。
#![allow(unused)] fn main() { let mut v = vec! [100, 32, 57]; for i in &mut v { *i += 50; } }
清单 8-9:对某个矢量中元素的可变引用进行迭代
要修改可变引用所指向的值,我们必须先使用 *
这个解引用运算符,the *
dereference operator,获取 i
中的值,然后才能使用 +=
这个运算符。我们将在第 15 章 “顺着指针找到值” 小节,详细介绍这个解引用运算符。
无论是可变还是不可变地遍历矢量,他们都是安全的,因为借用检查器的规则确保了这点。若我们在列表 8-7 与列表 8-8 的 for
循环体内,尝试插入或移除元素,我们就会得到与列表 8-6 中代码类似的编译器报错。到 for
循环所持有矢量的引用,会阻止对整个矢量的同时修改,the reference to the vector that the for
loop holds prevents simultaneous modification of the whole vector。
使用枚举存储多种类型
Using an Enum to Store Multiple Types
矢量值只能存储同一类型的值。这可能会造成不便;在某些情况下,我们肯定需要存储一些不同类型的条目列表。幸运的是,枚举变种是在同一枚举类型下定义的,因此当我们需要表示不同类型的元素一中类型时,我们可以定义并使用一个枚举!
例如,我们要从电子表格的某行中获取到一些值,该行中某些列包含了整数、浮点数与字符串。我们可以定义一个其变种包含不同值类型的枚举,而所有枚举变种将被视为同一类型:即该枚举的类型。然后,我们可以创建一个保存该枚举的矢量值,并最终保存了不同的类型。我们已在下面清单 8-9 中,演示了这点。
#![allow(unused)] 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
Rust 需要在编译时就知道,矢量值中将包含哪些类型,这样他才能准确地知道,堆上需要多少内存来存储各个元素。我们还必须显式指出,该向量容许哪些类型。若 Rust 允许某个矢量值存储任何类型,那么在对该矢量值的元素进行操作时,其中一种或多种类型就有可能导致错误。使用枚举与 match
表达式,意味着 Rust 将在编译时,确保每一种可能情形都得以处理,正如第 6 章中曾讨论的。
在咱们不直到在运行时程序将获得的要存储在某个矢量中的所有类型集时,那么这种枚举技巧就没有用。相反,咱们可以使用 特质对象,我们将在第 18 章中介绍。
现在我们已经讨论了矢量值的一些最常见用法,请务必查看 API 文档,了解标准库为 Vec<T>
定义的所有实用方法。例如,除 push
方法外,pop
方法会移除并返回最后的元素。
弃用矢量会弃用其元素
Dropping a Vector Drops Its Elements
与其他 struct
一样,在矢量值超出作用域时,他也会被释放,如同下面清单 8-10 中所注释的那样。
#![allow(unused)] fn main() { { let v = vec! [1, 2, 3, 4]; // 对 v 执行一些操作 } // 这里 v 就超出了作用域,而被释放掉 }
清单 8-10:展示矢量值及其元素在何处被弃用
当该矢量值被弃用时,其所有内容也会被弃用,这意味着他所保存的那些整数将被清理掉。借用检查器会确保到某个矢量值内容的全部引用,只有在这个矢量值本身有效期间才会被用到。
让我们移步下一集合类型: String
!
(End)