2.4 容器类型

Rust标准库std::collections提供了4种通用的容器类型,包含以下8种数据结构,如表2-2所示。

表2-2 Rust容器类型

028-01

下面对Rust编程中最常使用、后续实战中会反复用到的Vec、VecDeque和HashMap进行介绍。

2.4.1 Vec

Vec是一种动态可变长数组(简称动态数组),即在运行时可增长或者缩短数组的长度。动态数组在内存中开辟了一段连续内存块用于存储元素,且只能存储相同类型的元素。新加入的元素每次都会被添加到动态数组的尾部。动态数组根据添加顺序将数据存储为元素序列。序列中每个元素都分配有唯一的索引,元素的索引从0开始计数。

1. 动态数组的创建

创建动态数组有以下3种方式。

1)使用Vec::new函数创建空的动态数组,代码如下:

let mut v: Vec<i32> = Vec::new();

2)使用Vec::with_capacity函数创建指定容量的动态数组,代码如下:

let mut v: Vec<i32> = Vec::with_capacity(10);

动态数组的容量是指为存储元素所分配的内存空间量,而长度是动态数组中实际存储的元素个数。动态数组的长度小于其容量时,动态数组可以随意增长,但动态数组的长度超过其容量时就需要重新分配更大的容量。比如,容量为10的动态数组在存储10个以内的元素时,不会改变容量。当动态数组的长度增加到11时,就需要重新分配容量。这个分配过程会消耗一定的系统资源,因此应该尽可能根据初始元素个数以及增长情况来指定合理的容量。

3)使用vec!宏在创建动态数组的同时进行初始化,并且根据初始值自动推断动态数组的元素类型。代码如下所示,第1行代码创建一个空的动态数组,由于没有初始值,程序并不知道动态数组要存储什么类型的元素,因此需要声明元素类型。第2行代码创建的动态数组的初始值是1、2、3,程序会根据初始值自动推断元素类型是i32。第3行代码创建的动态数组包含10个元素,元素的初始值全部是0。

1  let mut v: Vec<i32> = vec![];
2  let mut v = vec![1, 2, 3];
3  let mut v = vec![0; 10];

2. 动态数组的修改

修改动态数组的常见操作有添加元素、修改指定索引的元素值和删除元素等。

1)使用push方法在动态数组的尾部添加新元素。代码清单2-10中,第2行代码中Vec::new函数创建了一个存储i32类型元素的动态数组,第3~5行代码中push方法将值添加到动态数组的尾部。

代码清单2-10 添加元素

 1  fn main() {
 2      let mut v: Vec<i32> = Vec::new();
 3      v.push(1);
 4      v.push(2);
 5      v.push(3);
 6
 7      println!("v: {:?}", v);
 8  }
 9
10  // v: [1, 2, 3]

2)使用“实例名[索引]”语法为指定索引的元素重新赋值。代码清单2-11中,第7行代码将索引为1的元素值改为5,第8行代码打印当前动态数组的元素,此时对应索引的元素值已被修改。

代码清单2-11 修改指定索引的元素值

 1  fn main() {
 2      let mut v: Vec<i32> = Vec::new();
 3      v.push(1);
 4      v.push(2);
 5      v.push(3);
 6
 7      v[1] = 5;
 8      println!("v: {:?}", v);
 9  }
10
11  // v: [1, 5, 3]

3)删除动态数组的元素有以下两种方式。

① 使用pop方法删除并返回动态数组的最后一个元素,如果数组为空则返回None。代码清单2-12中,第2行代码中Vec::with_capacity函数创建了一个指定容量为10、存储i32类型元素的动态数组。第7行代码中pop方法删除最后一个元素,并返回打印的Option类型的值Some(3)。

代码清单2-12 使用pop方法删除元素

 1  fn main() {
 2      let mut v: Vec<i32> = Vec::with_capacity(10);
 3      v.push(1);
 4      v.push(2);
 5      v.push(3);
 6
 7      println!("e: {:?}", v.pop());
 8      println!("v: {:?}", v);
 9  }
10
11  // e: Some(3)
12  // v: [1, 2]

② 使用remove方法删除并返回动态数组指定索引的元素,同时将其后面的所有元素向左移动一位。如果索引越界,将会导致程序错误。代码清单2-13中,第2行代码vec!宏创建一个包含初始值为1、2、3的动态数组。第4行代码中remove方法删除索引为1的元素,并返回打印的i32类型的值2。

代码清单2-13 使用remove方法删除元素

1  fn main() {
2      let mut v = vec![1, 2, 3];
3
4      println!("e: {}", v.remove(1));
5      println!("v: {:?}", v);
6  }
7
8  // e: 2
9  // v: [1, 3]

3. 动态数组的访问

访问动态数组的元素有以下两种方式。

1)使用“实例名[索引]”语法访问指定索引的元素。代码清单2-14中,第3行代码访问索引为2的元素,即动态数组的第3个元素。如果取消第4行的注释,由于索引已越界,将会导致程序错误。

代码清单2-14 使用索引语法访问元素

1  fn main() {
2      let v = vec![1, 2, 3];
3      println!("e: {}", v[2]);
4      // println!("e: {}", v[10]);
5  }
6
7  // e: 3

2)使用get方法以索引作为参数访问元素。代码清单2-15中,第3行代码访问索引为2的元素,返回值类型是Option<&i32>。第4行代码访问索引为10的元素,由于索引已越界,将会返回None。关于Option<T>类型的知识点将会在第5章详细介绍。

代码清单2-15 使用get方法访问元素

1  fn main() {
2      let v = vec![1, 2, 3];
3      println!("e: {:?}", v.get(2));
4      println!("e: {:?}", v.get(10));
5  }
6
7  // e: Some(3)
8  // e: None

除了访问动态数组指定索引的元素,我们还可以通过for循环遍历动态数组的所有元素。代码清单2-16中,第3~5行代码使用for循环遍历访问动态数组的所有元素。第8~11行代码使用for循环遍历动态数组中每一个元素的可变引用,并使用解引用操作符“*”来追踪和修改元素值。关于for循环、可变引用以及解引用操作符的知识点,我们将会在后续章节详细介绍。

代码清单2-16 遍历所有元素

 1  fn main() {
 2      let v = vec![10, 20, 30];
 3      for i in v {
 4          print!("{} ", i);
 5      }
 6    
 7      let mut v = vec![10, 20, 30];
 8      for i in &mut v {
 9          *i += 50;
10          print!("{} ", i);
11      }
12  }
13
14  // 10 20 30 60 70 80

2.4.2 VecDeque

双端队列是一种同时具有栈(先进后出)和队列(先进先出)特征的数据结构,适用于只能在队列两端进行添加或删除元素操作的应用场景。Rust使用VecDeque结构体表示双端队列,它定义在标准库的std::collections模块中。使用VecDeque结构体之前需要显式导入std::collections::VecDeque。

1. VecDeque的创建

创建VecDeque有以下两种方式。

1)使用VecDeque::new函数创建空的VecDeque,代码如下:

let mut v: VecDeque<u32> = VecDeque::new();

2)使用VecDeque::with_capacity函数创建指定容量的VecDeque,代码如下:

let mut v: VecDeque<u32> = VecDeque::with_capacity(10);

2. VecDeque的修改

修改VecDeque的常见操作有添加元素、修改指定索引的元素值和删除元素等。

1)使用push_front方法在队列的头部添加新元素,使用push_back方法在队列的尾部添加新元素。代码清单2-17中,第4行代码中VecDeque::new函数创建了一个存储u32类型元素的VecDeque。第5~7行代码中push_back方法将值添加到VecDeque的尾部。此时,v中的元素是[1, 2, 3]。第8~9行代码中push_front方法将值添加到VecDeque的头部。此时,v中的元素是[2, 1, 1, 2, 3]。

代码清单2-17 添加元素

 1  use std::collections::VecDeque;
 2
 3  fn main() {
 4      let mut v: VecDeque<u32> = VecDeque::new();
 5      v.push_back(1);
 6      v.push_back(2);
 7      v.push_back(3);
 8      v.push_front(1);
 9      v.push_front(2);
10
11      println!("v: {:?}", v);
12  }
13
14  // v: [2, 1, 1, 2, 3]

2)使用“实例名[索引]”语法为指定索引的元素重新赋值。代码清单2-18中,第9行代码将索引为1的元素值改为5,第10行代码打印当前VecDeque的元素,此时对应索引的元素值已被修改。

代码清单2-18 修改指定索引的元素值

 1  use std::collections::VecDeque;
 2
 3  fn main() {
 4      let mut v: VecDeque<u32> = VecDeque::new();
 5      v.push_back(1);
 6      v.push_back(2);
 7      v.push_back(3);
 8    
 9      v[1] = 5;
10      println!("v: {:?}", v);
11  }
12
13  // v: [1, 5, 3]

3)删除VecDeque的元素有以下两种方式。

① 使用pop_front方法删除并返回队列的头部元素,使用pop_back方法删除并返回队列的尾部元素。代码清单2-19中,使用第11行代码中的pop_back方法删除队列尾部元素,并返回打印的Option类型的值Some(3)。此时,v中的元素是[2, 1, 1, 2]。第12行代码中的pop_front方法删除队列头部元素,并返回打印的Option类型的值Some(2)。此时,v中的元素是[1, 1, 2]。

代码清单2-19 使用pop_front、pop_back方法删除元素

 1  use std::collections::VecDeque;
 2
 3  fn main() {
 4      let mut v: VecDeque<u32> = VecDeque::new();
 5      v.push_back(1);
 6      v.push_back(2);
 7      v.push_back(3);
 8      v.push_front(1);
 9      v.push_front(2);
10    
11      println!("e: {:?}", v.pop_back());
12      println!("e: {:?}", v.pop_front());
13      println!("v: {:?}", v);
14  }
15
16  // e: Some(3)
17  // e: Some(2)
18  // v: [1, 1, 2]

② 使用remove方法删除并返回队列指定索引的元素,同时将其后面的所有元素向左移动一位。如果索引越界,则返回None。代码清单2-20中,第4行代码中VecDeque::with_capacity函数创建一个指定容量为10、存储u32类型元素的VecDeque。第9行代码中remove方法删除索引为1的元素,并返回打印的Option类型的值Some(2)。第10行代码删除索引为5的元素,由于索引已越界,返回None。

代码清单2-20 使用remove方法删除元素

 1  use std::collections::VecDeque;
 2
 3  fn main() {
 4      let mut v: VecDeque<u32> = VecDeque::with_capacity(10);
 5      v.push_back(1);
 6      v.push_back(2);
 7      v.push_back(3);
 8    
 9      println!("e: {:?}", v.remove(1));
10      println!("e: {:?}", v.remove(5));
11      println!("v: {:?}", v);
12  }
13
14  // e: Some(2)
15  // e: None
16  // v: [1, 3]

3. VecDeque的访问

访问VecDeque的元素有以下两种方式。

1)使用“实例名[索引]”语法访问指定索引的元素。代码清单2-21中,第9行代码访问索引为0的元素,即VecDeque的第1个元素。如果取消第10行的注释,由于索引越界,将会导致程序错误。

代码清单2-21 使用索引语法访问元素

 1  use std::collections::VecDeque;
 2
 3  fn main() {
 4      let mut v: VecDeque<u32> = VecDeque::new();
 5      v.push_back(1);
 6      v.push_back(2);
 7      v.push_back(3);
 8    
 9      println!("e: {}", v[0]);
10      // println!("e: {}", v[10]);
11  }
12
13  // e: 1

2)使用get方法以索引作为参数访问元素。代码清单2-22中,第9行代码访问索引为0的元素,返回值类型是Option<&i32>。第10行代码访问索引为10的元素,由于索引越界,将会返回None。

代码清单2-22 使用get方法访问元素

 1  use std::collections::VecDeque;
 2
 3  fn main() {
 4      let mut v: VecDeque<u32> = VecDeque::new();
 5      v.push_back(1);
 6      v.push_back(2);
 7      v.push_back(3);
 8    
 9      println!("e: {:?}", v.get(0));
10      println!("e: {:?}", v.get(10));
11  }
12
13  // e: Some(1)
14  // e: None

2.4.3 HashMap

哈希表(HashMap)是基于哈希算法来存储键-值对的集合,其中所有的键必须是同一类型,所有的值也必须是同一类型,不允许有重复的键,但允许不同的键有相同的值。Rust使用HashMap结构体表示哈希表,它定义在标准库的std::collections模块中。使用HashMap结构体之前需要显式导入std::collections::HashMap。

1. HashMap的创建

创建HashMap有以下两种方式。

1)使用HashMap::new函数创建空的HashMap,代码如下:

let mut map: HashMap<&str, i32> = HashMap::new();

2)使用HashMap::with_capacity函数创建指定容量的HashMap,代码如下:

let mut map: HashMap<&str, i32> = HashMap::with_capacity(10);

2. HashMap的修改

由于HashMap中元素是键-值对的特殊性,要修改HashMap就必须考虑键已经有值的情况。修改HashMap的常见操作有插入/更新键-值对、只在键没有对应值时插入键-值对、以新旧两值的计算结果来更新键-值对和删除键-值对。

1)使用insert方法在HashMap中插入或更新键-值对。如果键不存在,执行插入操作并返回None。如果键已存在,执行更新操作,将对应键的值更新并返回旧值。

代码清单2-23中,第4行代码中HashMap::new函数创建一个空的HashMap,可存储由&str类型的键与i32类型的值组成的键-值对。第6行代码由于键zhangsan不存在,insert方法执行插入操作,返回None。第12行代码由于键zhangsan已经存在,insert方法执行更新操作,键值由97变成79,返回Some(97)。

代码清单2-23 使用insert方法插入/更新键-值对

 1  use std::collections::HashMap;
 2
 3  fn main() {
 4      let mut map: HashMap<&str, i32> = HashMap::new();
 5
 6      let zhangsan1 = map.insert("zhangsan", 97);
 7      map.insert("lisi", 86);
 8    
 9      println!("{:?}", zhangsan1);
10      println!("{:?}", map);
11
12      let zhangsan2 = map.insert("zhangsan", 79);
13      println!("{:?}", zhangsan2);
14      println!("{:?}", map);
15  }
16
17  // None
18  // {"lisi": 86, "zhangsan": 97}
19  // Some(97)
20  // {"lisi": 86, "zhangsan": 79}

2)使用entry和or_insert方法检查键是否有对应值,没有对应值就插入键-值对,已有对应值则不执行任何操作。entry方法以键为参数,返回值是一个枚举类型Entry。Entry类型的or_insert方法以值为参数,在键有对应值时不执行任何操作。在键没有对应值时,将键与值组成键-值对插入HashMap。

代码清单2-24中,第6行代码中entry方法会检查键zhangsan是否有对应值,没有对应值就插入该键-值对。第10行代码中entry方法会再次检查键zhangsan是否有对应值,发现键zhangsan已有对应值97,那就不执行任何操作直接返回这个值的类型Entry,因此键zhangsan的对应值不变。

代码清单2-24 使用entry方法插入键-值对

 1  use std::collections::HashMap;
 2
 3  fn main() {
 4      let mut map: HashMap<&str, i32> = HashMap::new();
 5    
 6      map.entry("zhangsan").or_insert(97);
 7      map.entry("lisi").or_insert(86);
 8      println!("{:?}", map);
 9
10      map.entry("zhangsan").or_insert(79);
11      println!("{:?}", map);
12  }
13
14  // {"lisi": 86, "zhangsan": 97}
15  // {"lisi": 86, "zhangsan": 97}

3)以新旧两值的计算结果来更新键-值对是指找到一个键对应值,结合新旧两值进行某些计算处理,以计算结果来更新键对应值。比如,老师发现本次考试试卷上出现了一道错题,决定为所有学生的分数都加上2分,那么就可以将每个学生的名字作为键,将对应分数加上2。

代码清单2-25中,第11行代码中iter_mut方法会返回由HashMap中所有键-值对的可变引用组成的迭代器,通过for循环遍历迭代器。由于只针对值做处理,因此只取&mut i32类型的val,通过解引用操作符“*”对val进行赋值操作。关于迭代器、for循环、可变引用以及解引用操作符的知识点,我们将会在后续章节详细介绍。

代码清单2-25 修改HashMap元素

 1  use std::collections::HashMap;
 2
 3  fn main() {
 4      let mut map: HashMap<&str, i32> = HashMap::new();
 5    
 6      map.insert("zhangsan", 97);
 7      map.insert("lisi", 86);
 8      map.insert("wangwu", 55);    
 9      println!("{:?}", map);
10
11      for (_, val) in map.iter_mut() {
12          *val += 2;
13      }
14      println!("{:?}", map);
15  }
16
17  // {"zhangsan": 97, "lisi": 86, "wangwu": 55}
18  // {"zhangsan": 99, "lisi": 88, "wangwu": 57}

4)使用remove方法删除并返回指定的键-值对,如果不存在就返回None。代码清单2-26中,第11行代码中的remove方法删除键wangwu的对应值,并将值返回,打印Option类型的值Some(55)。

代码清单2-26 使用remove方法删除键-值对

 1  use std::collections::HashMap;
 2
 3  fn main() {
 4      let mut map: HashMap<&str, i32> = HashMap::new();
 5    
 6      map.insert("zhangsan", 97);
 7      map.insert("lisi", 86);
 8      map.insert("wangwu", 55);
 9      println!("{:?}", map);
10    
11      let result = map.remove("wangwu");
12      println!("{:?}", map);
13      println!("{:?}", result);
14  }
15
16  // {"wangwu": 55, "lisi": 86, "zhangsan": 97}
17  // {"lisi": 86, "zhangsan": 97}
18  // Some(55)

3. HashMap的访问

访问HashMap中指定的键-值对有以下两种方式。

1)使用“实例名[键]”语法访问指定的键-值对。如果键不存在,将会导致程序错误。代码清单2-27中,第7行代码访问键zhangsan的对应值,由于键zhangsan存在,可以正常访问到对应值。如果取消第8行的注释,由于键wangwu不存在,将会导致程序错误。

代码清单2-27 使用“实例名[键]”语法访问键-值对

 1  use std::collections::HashMap;
 2
 3  fn main() {
 4      let mut map: HashMap<&str, i32> = HashMap::new();
 5      map.insert("zhangsan", 97);
 6    
 7      println!("zhangsan: {}", map["zhangsan"]);
 8      // println!("wangwu: {}", map["wangwu"]);
 9  }
10
11  // zhangsan: 97

2)使用get方法以键为参数访问指定的键-值对。代码清单2-28中,第7行代码访问键zhangsan的对应值,返回值类型是Option<&i32>。第8行代码访问键wangwu的对应值,由于键wangwu不存在,将会返回None。

代码清单2-28 使用get方法为参数访问指定的键-值对

 1  use std::collections::HashMap;
 2
 3  fn main() {
 4      let mut map: HashMap<&str, i32> = HashMap::new();
 5      map.insert("zhangsan", 97);
 6    
 7      println!("zhangsan: {:?}", map.get("zhangsan"));
 8      println!("wangwu: {:?}", map.get("wangwu"));
 9  }
10
11  // zhangsan: Some(97)
12  // wangwu: None