Rust 从 0 到 1- 集合 -Vector
Rust 标准库中包含了一类我们经常会用到,也非常有用的数据结构-集合(collections)。就像它的名字一样,集合可以包含多个值,并且不同于数组和元组类型,集合指向的数据是储存在堆上的,也就是说数据的大小不必在编译时就知道,还可以在运行时改变,即增大或缩小。每种集合的特点都不一样,譬如占用的空间、存储方式、索引方式等,因此根据实际的场景选择合适的集合是一项技巧。因此,后面我们将详细介绍三个在 Rust 中最为常用的集合:
vector 顺序的存储一个数据集合。
string 字符的集合。我们之前的例子中使用过 String 类型,不过将对它进行更深入的了解。
hash map 使用 key-value 的形式存储数据。是 map 数据结构的一种实现。
标准库还提供了很多其他类型的集合,大家可以参考官方文档。
下面我们将介绍第一个集合类型 Vec<T>,也被称为 vector。vector 允许我们在一个数据结构中储存多个值,并且在内存会使用一段连续的空间彼此相邻地进行排列。注意,vector 只能储存相同类型的值。它在使用到“列表”场景下非常实用,譬如购物车中商品的价格列表。
创建 vector
创建一个新的空 vector,可以调用 Vec::new 函数:
注意这里我们指定了存储的数据类型,这是由于这是一个空 vector,Rust 无法知道我们想要储存什么类型的数据,这个在前面说过,Rust 是不允许的。vector 是用泛型实现的,这个会在后面章节介绍,目前,我们只需要知道 Vec 是一个由标准库提供的类型,它可以存放任何类型,而当 Vec 存放某个特定类型时,类型在尖括号中定义,譬如,例子中的 Vec<i32> ,用于存放 i32 类型的元素。
在实际使用的时候,我们一般不需在定义的时候指定类型,因为在后面赋值的时候 Rust 就可以根据赋值推断出数据的类型。而且为了方便 Rust 提供了 vec! 宏。这个宏会根据我们提供的值初始化一个新的 Vec:
修改 vector
我们可以使用 push 和 remove 方法向 vector 中增加或删除元素:
和前面讨论变量的章节中提到的一样,如果想要能够改变 vector 类型变量的值,必须使用 mut 关键字。(由于后面的代码向 vector 放入了 i32 类型的值,Rust 会根据数据推断出类型,所以不需要在定义时声明 Vec<i32>)。
丢弃 vector
类似其他结构体,vector 在其离开作用域时就会被释放:
当 vector 离开作用域的时会被丢弃,同时其所有元素也会被丢弃。当 vector 的元素被使用时,情况就会变得复杂,下面让我们将进行讨论!
读取 vector
有两种方法引用 vector 中储存的值,使用索引或 get 方法:
例子中有两个需要注意的地方。首先,索引是从 0 开始,因此我们使用索引 2 来获取第三个元素;其次,这两种不同的方式的返回值类型是不同的,一种返回的是一个引用;一种返回的是 Option<&T>。Option<&T> 类型给了我们一种优雅的方式处理越界的情况,在上例中假设我们访问的是 第 100 个元素,第一种方式运行时会报错,程序崩溃;而第二种方式将会更友好的告诉我们元素不存在:
当 get 方法被传递了一个超过边界的索引时,它不会 panic 而是返回 None。当存在出现超过 vector 范围的访问属于可能的时候可以考虑使用 get。这样我们就可以向前面介绍 Option 时那样处理 Some 和 None 的逻辑。譬如,索引可能来源于用户的输入。如果他们输入了一个过大的数字那么我们就会得到 None 值,在 None 的处理逻辑里我们可以告诉用户当前 vector 元素的数量并再提示它们输入一个有效的值。以免因为输入错误而导致程序崩溃!
由于我们是引用的 vector 中的元素,前面我们介绍的所有权和借用规则 Rust 都会用来进行检查,以确保对 vector 元素的引用保持有效。譬如,相同作用域中不能同时存在可变和不可变引用:
上例中,我们获取了 vector 的第一个元素的不可变引用,然后尝试在 vector 中增加一个元素,这在编译的时候就会报错,因为这会造成“悬空引用”。这是因为在 vector 中增加新元素时,如果当前申请的连续的堆空间不足够存放,将会请求分配新内存空间并所有元素拷贝到新的空间中。这时,我们所定义的引用就指向了被释放的内存,这是 Rust 所不允许的。
遍历 vector
如果想要遍历 vector 中的每一个元素,我们可以使用 for 循环而无需通过索引逐个的访问:
我们也可以遍历一个可变 vector 的每一个元素的可变引用来修改元素的值:
为了修改可变引用所指向的值,我们需要使用解引用运算符 * 。后面章节会详细介绍解引用运算符。
使用枚举存储多种类型
开始我们说过 vector 只能储存相同类型的值。但是有些时候我们会需要储存不同类型的值。由于枚举的成员都被定义为相同的枚举类型,因此这时候我们可以借用枚举来进行处理。譬如,我们要读取电子表格一行中的值,而有些列是数字,有些列浮点值,还可能有些列是字符串。那么我们可以定义一个枚举,其成员为这些不同的类型,这样最终就能够将不同类型的值存入 vector 了:
由于 Rust 需要知道储存每个元素到底需要多少内存,因此在编译时就必须能准确的知道 vector 中元素的类型。另外一个好处是,由于准确的知道 vector 元素的类型,可以避免当对 vector 元素执行操作时可能会造成的错误。使用枚举和 match 让 Rust 能在编译时就保证我们处理了所有可能的情况。
如果运行时会储存进 vector 的元素类型我们无法事前知道(我们不可能去穷举所有类型),那么枚举就不适用了。这个时候,可以使用 trait(后面章节会介绍)。
推荐大家去看一下官方 API 文档标准库中 Vec 定义的方法。譬如,pop 方法,它会移除并返回 vector 末尾的元素。
版权声明: 本文为 InfoQ 作者【山】的原创文章。
原文链接:【http://xie.infoq.cn/article/1f051739471043929e88f1308】。文章转载请联系作者。
评论