写点什么

Rust 从 0 到 1- 集合 -Vector

用户头像
关注
发布于: 2021 年 04 月 29 日
Rust从0到1-集合-Vector

Rust 标准库中包含了一类我们经常会用到,也非常有用的数据结构-集合(collections)。就像它的名字一样,集合可以包含多个值,并且不同于数组和元组类型,集合指向的数据是储存在堆上的,也就是说数据的大小不必在编译时就知道,还可以在运行时改变,即增大或缩小。每种集合的特点都不一样,譬如占用的空间、存储方式、索引方式等,因此根据实际的场景选择合适的集合是一项技巧。因此,后面我们将详细介绍三个在 Rust 中最为常用的集合:

  • vector 顺序的存储一个数据集合。

  • string 字符的集合。我们之前的例子中使用过 String 类型,不过将对它进行更深入的了解。

  • hash map 使用 key-value 的形式存储数据。是 map 数据结构的一种实现。

标准库还提供了很多其他类型的集合,大家可以参考官方文档。

下面我们将介绍第一个集合类型 Vec<T>,也被称为 vector。vector 允许我们在一个数据结构中储存多个值,并且在内存会使用一段连续的空间彼此相邻地进行排列。注意,vector 只能储存相同类型的值。它在使用到“列表”场景下非常实用,譬如购物车中商品的价格列表。

创建 vector

创建一个新的空 vector,可以调用 Vec::new 函数:

let v: Vec<i32> = Vec::new();
复制代码

注意这里我们指定了存储的数据类型,这是由于这是一个空 vector,Rust 无法知道我们想要储存什么类型的数据,这个在前面说过,Rust 是不允许的。vector 是用泛型实现的,这个会在后面章节介绍,目前,我们只需要知道 Vec 是一个由标准库提供的类型,它可以存放任何类型,而当 Vec 存放某个特定类型时,类型在尖括号中定义,譬如,例子中的 Vec<i32> ,用于存放 i32 类型的元素。

在实际使用的时候,我们一般不需在定义的时候指定类型,因为在后面赋值的时候 Rust 就可以根据赋值推断出数据的类型。而且为了方便 Rust 提供了 vec! 宏。这个宏会根据我们提供的值初始化一个新的 Vec:

let v = vec![1, 2, 3];
复制代码

修改 vector

我们可以使用 push 和 remove 方法向 vector 中增加或删除元素:

let mut v = Vec::new();
v.push(5);v.push(6);v.push(7);v.push(8);
v[0] = 9;
v.remove(0);
复制代码

和前面讨论变量的章节中提到的一样,如果想要能够改变 vector 类型变量的值,必须使用 mut 关键字。(由于后面的代码向 vector 放入了 i32 类型的值,Rust 会根据数据推断出类型,所以不需要在定义时声明 Vec<i32>)。

丢弃 vector

类似其他结构体,vector 在其离开作用域时就会被释放:


{ let v = vec![1, 2, 3, 4]; // do stuff with v} // <- v 离开作用
复制代码

当 vector 离开作用域的时会被丢弃,同时其所有元素也会被丢弃。当 vector 的元素被使用时,情况就会变得复杂,下面让我们将进行讨论!

读取 vector

有两种方法引用 vector 中储存的值,使用索引或 get 方法:

let v = vec![1, 2, 3, 4, 5];
let third: &i32 = &v[2];println!("The third element is {}", third);
match v.get(2) { Some(third) => println!("The third element is {}", third), None => println!("There is no third element."),}
复制代码

例子中有两个需要注意的地方。首先,索引是从 0 开始,因此我们使用索引 2 来获取第三个元素;其次,这两种不同的方式的返回值类型是不同的,一种返回的是一个引用;一种返回的是 Option<&T>。Option<&T> 类型给了我们一种优雅的方式处理越界的情况,在上例中假设我们访问的是 第 100 个元素,第一种方式运行时会报错,程序崩溃;而第二种方式将会更友好的告诉我们元素不存在:

let v = vec![1, 2, 3, 4, 5];
let third: &i32 = &v[100];println!("The third element is {}", third);
match v.get(100) { Some(third) => println!("The third element is {}", third), None => println!("There is no third element."),}
复制代码

当 get 方法被传递了一个超过边界的索引时,它不会 panic 而是返回 None。当存在出现超过 vector 范围的访问属于可能的时候可以考虑使用 get。这样我们就可以向前面介绍 Option 时那样处理 Some 和 None 的逻辑。譬如,索引可能来源于用户的输入。如果他们输入了一个过大的数字那么我们就会得到 None 值,在 None 的处理逻辑里我们可以告诉用户当前 vector 元素的数量并再提示它们输入一个有效的值。以免因为输入错误而导致程序崩溃!

由于我们是引用的 vector 中的元素,前面我们介绍的所有权和借用规则 Rust 都会用来进行检查,以确保对 vector 元素的引用保持有效。譬如,相同作用域中不能同时存在可变和不可变引用:

let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0];
v.push(6);
println!("The first element is: {}", first);
复制代码

上例中,我们获取了 vector 的第一个元素的不可变引用,然后尝试在 vector 中增加一个元素,这在编译的时候就会报错,因为这会造成“悬空引用”。这是因为在 vector 中增加新元素时,如果当前申请的连续的堆空间不足够存放,将会请求分配新内存空间并所有元素拷贝到新的空间中。这时,我们所定义的引用就指向了被释放的内存,这是 Rust 所不允许的。

遍历 vector

如果想要遍历 vector 中的每一个元素,我们可以使用 for 循环而无需通过索引逐个的访问:

let v = vec![100, 32, 57];for i in &v {    println!("{}", i);}
复制代码

我们也可以遍历一个可变 vector 的每一个元素的可变引用来修改元素的值:

let mut v = vec![100, 32, 57];for i in &mut v {    *i += 50;}
复制代码

为了修改可变引用所指向的值,我们需要使用解引用运算符 * 。后面章节会详细介绍解引用运算符。

使用枚举存储多种类型

开始我们说过 vector 只能储存相同类型的值。但是有些时候我们会需要储存不同类型的值。由于枚举的成员都被定义为相同的枚举类型,因此这时候我们可以借用枚举来进行处理。譬如,我们要读取电子表格一行中的值,而有些列是数字,有些列浮点值,还可能有些列是字符串。那么我们可以定义一个枚举,其成员为这些不同的类型,这样最终就能够将不同类型的值存入 vector 了:

enum SpreadsheetCell {    Int(i32),    Float(f64),    Text(String),}
let row = vec![ SpreadsheetCell::Int(3), SpreadsheetCell::Text(String::from("blue")), SpreadsheetCell::Float(10.12),];
复制代码

由于 Rust 需要知道储存每个元素到底需要多少内存,因此在编译时就必须能准确的知道 vector 中元素的类型。另外一个好处是,由于准确的知道 vector 元素的类型,可以避免当对 vector 元素执行操作时可能会造成的错误。使用枚举和 match 让 Rust 能在编译时就保证我们处理了所有可能的情况。

如果运行时会储存进 vector 的元素类型我们无法事前知道(我们不可能去穷举所有类型),那么枚举就不适用了。这个时候,可以使用 trait(后面章节会介绍)。

推荐大家去看一下官方 API 文档标准库中 Vec 定义的方法。譬如,pop 方法,它会移除并返回 vector 末尾的元素。

发布于: 2021 年 04 月 29 日阅读数: 16
用户头像

关注

公众号"山 顽石"欢迎大家关注;) 2021.03.23 加入

IT老兵,关注Rust、Java、前端、架构、大数据、AI、算法等;平时喜欢美食、旅游、宠物、健身、篮球、乒乓球等。希望能和大家交流分享。

评论

发布
暂无评论
Rust从0到1-集合-Vector