8.6 HashMap Pt.2:更新HashMap
8.6.0. 本章内容
第八章主要讲的是 Rust 中常见的集合。Rust 提供了很多集合类型的数据结构,这些集合可以包含很多值。但是第八章所讲的集合与数组和元组有所不同。
第八章中的集合是存储在堆内存上而非栈内存上的,这也意味着这些集合的数据大小无需在编译时就确定,在运行时它们可以动态地变大或变小。
本章主要会讲三种集合:Vector、String 和 HashMap(本文)。
8.6.1. 更新 HashMap
HashMap 的大小可变,指的是其中的键值对数量可变。但是在任意时刻,一个键只能对应一个值。当想要更新 HashMap 中的数据时,可能有这么几种情况:
-
想要更新的键在
HashMap中已经存在对应的值:- 用新的值替换现有的值
- 保留现有的值,忽略新的值
- 合并现有的值和新的值,也就是说对现有的值进行修改
-
键不存在:添加一对键和值
1. 替换现有的值
如果向 HashMap 插入一对键值对,但键已经存在,程序就会把新值赋给这个键,覆盖旧值。如下例:
use std::collections::HashMap;
fn main() {
let mut scores = HashMap::new();
scores.insert(String::from("dev1ce"), 0);
scores.insert(String::from("dev1ce"), 60);
println!("{:?}", scores);
}
这里为同一个键赋了两次值:第一次是 0,第二次是 60。第一次的值会被第二次覆盖,也就是说最终 "dev1ce" 对应的值是 60。
输出:
{"dev1ce": 60}
2. 只在键不存在值时才插入
这是最常见的情况。对于这种情况,首先需要检查原 HashMap 中是否已经存在这个键;如果不存在,再插入新值。
Rust 提供了 entry 方法来检查原 HashMap 中是否已经存在这个键。它的参数是键,返回值是一个 Entry 枚举,表示值是否存在。看个例子:
use std::collections::HashMap;
fn main() {
let mut scores = HashMap::new();
scores.insert(String::from("dev1ce"), 0);
let e = scores.entry(String::from("dev1ce"));
println!("{:?}", e);
}
这是键已经存在的情况。输出:
Entry(OccupiedEntry { key: "dev1ce", value: 0, .. })
也就是说,如果键已经存在,那么 entry 方法会返回一个占用中的 entry(OccupiedEntry),并关联已经存在的键值对。
再试一下键不存在的情况。代码如下:
use std::collections::HashMap;
fn main() {
let mut scores = HashMap::new();
scores.insert(String::from("dev1ce"), 0);
let e = scores.entry(String::from("Zywoo"));
println!("{:?}", e);
}
输出:
Entry(VacantEntry("Zywoo"))
如果键不存在,就会返回一个空缺的 entry(VacantEntry),并关联这个新的键。
现在有办法检查原 HashMap 中是否已经存在这个键了,那么如何根据是否存在来决定插入或不插入呢?
Rust 在 Entry 上提供了 or_insert 方法,其参数是想要添加的值。它会根据 entry 是占用还是空缺来决定是否插入:如果 entry 已被占用(键已存在),就保留现有值、不插入新值;如果 entry 空缺(键不存在),就插入传入的值。最重要的一点是,它有返回值:该键对应值的可变引用。 如果键已存在,就返回 HashMap 中原有值的可变引用;如果键不存在,就先插入键值对,再返回插入值的可变引用。利用这个特性可以实现一些简单的计数器(后文会讲)。
看下例子:
use std::collections::HashMap;
fn main() {
let mut scores = HashMap::new();
scores.insert(String::from("dev1ce"), 0);
scores.entry(String::from("Zywoo")).or_insert(100);
scores.entry(String::from("dev1ce")).or_insert(60);
println!("{:?}", scores);
}
- 第一个
entry语句查找"Zywoo",没有找到,就返回空缺的 entry;or_insert会根据该键和传入的参数100,创建("Zywoo", 100)这个键值对。 - 第二个
entry语句查找"dev1ce",已经找到,就返回占用中的 entry;or_insert不会插入新值,因此("dev1ce", 0)保持不变。
输出(键的顺序可能不同):
{"Zywoo": 100, "dev1ce": 0}
如果这么讲还有些复杂,那么你可以把 scores.entry(String::from("Zywoo")).or_insert(100); 看作两行代码:
#![allow(unused)]
fn main() {
let e = scores.entry(String::from("Zywoo"));
e.or_insert(100);
}
3. 基于现有值来更新
先看例子:
use std::collections::HashMap;
fn main() {
let text = "That's one small step for [a] man, one giant leap for mankind.";
let mut map = HashMap::new();
for word in text.split_whitespace() {
let count = map.entry(word).or_insert(0);
*count += 1;
}
println!("{:#?}", map);
}
- 首先声明了一个包含一句话的字符串字面值,赋给
text。 - 然后创建了名为
map的HashMap。 - 接着使用
for循环。text.split_whitespace()把text分割成字符串迭代器,再用for来遍历。 - 遍历时,判断每个单词是否已出现在
map中:出现了就不插入新值,没出现就插入0作为该键的新值。关键是理解count:因为or_insert的返回值是该键对应值的可变引用,所以每出现一次这个单词,就把可变引用解引用后加1,相当于完成一次计数。
8.6.2. Hash 函数
在默认情况下,HashMap 使用加密强度较高的哈希函数,可以抵抗拒绝服务(DoS)攻击。但这种函数并不是可用的最快哈希算法,它的优势是具有更好的安全性。如果觉得它性能不够好,也可以指定不同的 hasher 来切换到另一个函数。hasher 指的是实现了 BuildHasher trait 的类型。