8.5 HashMap Pt.1:HashMap的定义、创建、合并与访问
8.5.0. 本章内容
第八章主要讲的是 Rust 中常见的集合。Rust 提供了很多集合类型的数据结构,这些集合可以包含很多值。但是第八章所讲的集合与数组和元组有所不同。
第八章中的集合是存储在堆内存上而非栈内存上的,这也意味着这些集合的数据大小无需在编译时就确定,在运行时它们可以动态地变大或变小。
本章主要会讲三种集合:Vector、String 和 HashMap(本文)。
8.5.1. 什么是 HashMap
HashMap 的形式是 HashMap<K, V>,其中 K 代表键(key),V 代表值(value)。HashMap 以键值对的形式存储数据,一个键对应一个值。很多语言都支持这样的集合数据结构,但叫法不一定相同——例如 C# 中相同概念的数据结构叫字典(dictionary)。
HashMap 的内部实现使用了哈希函数,这个函数决定了如何在内存中存储键与值。
在 Vector 中我们使用索引来访问数据,但有的时候你想通过键来查找数据(键可以是任何实现了 Eq 和 Hash trait 的类型),而不是通过索引,或者说你不清楚这个数据在哪个索引上。这种情况就可以使用 HashMap。
需要注意的是,HashMap 是同构的,也就是说在一个 HashMap 中,所有的键必须是同一类型,所有的值必须是同一类型。
8.5.2. 创建 HashMap
- 由于
HashMap不常用,所以 Rust 并没有把它放进预导入模块(prelude)。使用前需要引入HashMap,在文件开头写上:use std::collections::HashMap; - 创建空的
HashMap使用HashMap::new()函数 - 添加数据使用
insert()方法
看个例子:
use std::collections::HashMap;
fn main() {
let mut scores: HashMap<String, i32> = HashMap::new();
}
在这里创建了一个名为 scores 的变量来存储 HashMap。由于 Rust 是强类型语言,它必须知道你在 HashMap 里存储什么数据类型。又因为没有前后文可供编译器推断,所以在声明时就必须把键和值的数据类型显式声明出来。在这段代码中,scores 的键被设为 String,值被设为 i32。
当然,如果你在后文给这个 HashMap 添加了数据,Rust 就会根据插入的数据自动推断键和值的数据类型。添加数据使用 insert() 方法。例子如下:
use std::collections::HashMap;
fn main() {
let mut scores = HashMap::new();
scores.insert(String::from("dev1ce"), 0);
}
因为在第 5 行往 scores 里插入了键值对,且键 String::from("dev1ce") 是 String 类型,值 0 是 i32(Rust 默认整数类型是 i32),所以编译器就会推断出 scores 是一个 HashMap<String, i32>,因此第四行在声明时就不需要显式声明类型了。
8.5.3. 将两个 Vector 合为一个 HashMap
在元素类型为元组的 Vector 上,可以使用 collect 方法来构建 HashMap。换个说法:假如你有两个 Vector,这两个 Vector 上的所有值都有一一对应关系,就可以使用 collect,把一个 Vector 里的数据作为键,另一个作为值,放到 HashMap 里。如下例:
use std::collections::HashMap;
fn main() {
let player = vec![String::from("dev1ce"), String::from("Zywoo")];
let initial_scores = vec![0, 100];
let scores: HashMap<_, _> = player.into_iter().zip(initial_scores).collect();
}
player这个Vector用来存储选手名字,里面的元素是String类型。initial_scores这个Vector用来存储每个选手对应的得分。player.into_iter()以及传给zip的initial_scores(也会变成迭代器)会从这两个Vector中取出拥有所有权的值。使用.zip()可以创建“player中元素在前、initial_scores中元素在后”的元组序列。如果想交换元素位置,把代码中的两个迭代器互换位置即可。然后再使用.collect()把这些元组转换成HashMap。因为使用了into_iter,HashMap会获得这些值的所有权(原来的两个Vector会被消耗掉)。- 最后要注意的一点是:
.collect()支持转换为很多数据结构。如果写代码时不显式声明其类型,程序就会报错。这里指明了类型是HashMap<_, _>。<>中的两个数据类型,编译器可以根据代码(也就是两个Vector的类型)来推断,所以这里可以写_占位符让它自行推断。
8.5.4. HashMap 和所有权
对于实现了 Copy trait 的数据类型(例如 i32 以及绝大多数简单数据类型),值会被复制到 HashMap 中,原先的变量仍然可用。对于没有实现 Copy 的类型(例如 String),所有权会被移交给 HashMap。
如果将引用插入到 HashMap,值本身就不会移动。在 HashMap 的有效期间,被引用的值必须保持有效。
8.5.5. 访问 HashMap 中的值
访问值可以使用 get 方法。get 方法的参数是 HashMap 的键,返回值是 Option<&V> 枚举。看个例子:
use std::collections::HashMap;
fn main() {
let mut scores = HashMap::new();
scores.insert(String::from("dev1ce"), 0);
scores.insert(String::from("Zywoo"), 100);
let player_name = String::from("dev1ce");
let score = scores.get(&player_name);
match score {
Some(score) => println!("{}", score),
None => println!("Player not found"),
};
}
- 首先创建了一个空的
HashMap叫做scores,然后通过insert往里面添加了两个键值对("dev1ce", 0)和("Zywoo", 100),键类型是String,值类型是i32。 - 然后声明了名为
player_name的String变量,其值为"dev1ce"。 - 接着通过
HashMap上的get方法,在scores中查找player_name这个键所对应的值(&表示引用)。但由于get返回的是Option枚举,所以这里先把这个Option值赋给score,后面再来解包。 - 最后使用
match表达式来处理score。如果找到了对应的值,score就是Some变体,把Some关联的值绑定到score上,然后再打印出来。如果找不到,score就是None变体,这时就会打印"Player not found"。
输出:
0
8.5.6. 遍历 HashMap
遍历 HashMap 一般使用 for 循环。如下例:
use std::collections::HashMap;
fn main() {
let mut scores = HashMap::new();
scores.insert(String::from("dev1ce"), 0);
scores.insert(String::from("Zywoo"), 100);
for (k, v) in &scores {
println!("{}: {}", k, v);
}
}
这个 for 循环使用的是 HashMap 的引用,也就是 &scores,因为通常遍历之后还要继续使用这个 HashMap,所以使用引用就不会失去所有权。前面的 (k, v) 是模式匹配:第一个值是键,赋给 k;第二个是值,赋给 v。
一种可能的输出如下(遍历顺序是任意的,不同运行之间可能不同):
dev1ce: 0
Zywoo: 100