2.4 容器类型
Rust标准库std::collections提供了4种通用的容器类型,包含以下8种数据结构,如表2-2所示。
表2-2 Rust容器类型
下面对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