Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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
  • 然后创建了名为 mapHashMap
  • 接着使用 for 循环。text.split_whitespace()text 分割成字符串迭代器,再用 for 来遍历。
  • 遍历时,判断每个单词是否已出现在 map 中:出现了就不插入新值,没出现就插入 0 作为该键的新值。关键是理解 count:因为 or_insert 的返回值是该键对应值的可变引用,所以每出现一次这个单词,就把可变引用解引用后加 1,相当于完成一次计数。

8.6.2. Hash 函数

在默认情况下,HashMap 使用加密强度较高的哈希函数,可以抵抗拒绝服务(DoS)攻击。但这种函数并不是可用的最快哈希算法,它的优势是具有更好的安全性。如果觉得它性能不够好,也可以指定不同的 hasher 来切换到另一个函数。hasher 指的是实现了 BuildHasher trait 的类型。