HashMap

我们对 Index/IndexMut 的实现并不理想:我们需要遍历整个 Vec 来通过 ID 获取工单;算法复杂度为 O(n),其中 n 是存储中工单的数量。

我们可以通过使用不同的数据结构 HashMap<K, V> 来改善这一情况来存储工单。

#![allow(unused)]
fn main() {
use std::collections::HashMap;

// 类型推导允许我们省略显式的类型签名(在这个例子中将是 `HashMap<String, String>`)。
let mut book_reviews = HashMap::new();

book_reviews.insert(
    "哈克贝利·费恩历险记".to_string(),
    "我最喜欢的书.".to_string(),
);
}

HashMap 通过键值对工作。它对两者都具有泛型性:K 是键类型的泛型参数,而 V 是值类型的泛型参数。

插入、检索和移除的预期成本是恒定的,即 O(1)。这听起来非常适合我们的应用场景,不是吗?

键的要求

虽然 HashMap 的结构定义上没有特征界限,但你可以在其方法上找到一些。以 insert 为例:

#![allow(unused)]
fn main() {
// 略微简化
impl<K, V> HashMap<K, V>
where
    K: Eq + Hash,
{
    pub fn insert(&mut self, k: K, v: V) -> Option<V> {
        // [...]
    }
}
}

键的类型必须实现 EqHash 特征。

Hash

哈希函数(或散列器)将一个潜在无限的值集合(例如,所有可能的字符串)映射到一个有限范围内(例如,一个 u64 值)。有许多不同的哈希函数,各有不同的属性(速度、碰撞风险、可逆性等)。

顾名思义,HashMap 在幕后使用哈希函数。它对你的键进行哈希运算,然后使用该哈希值来存储/检索相关的值。这种策略要求键类型必须是可哈希的,因此在 K 上有 Hash 特征界限。

你可以在 std::hash 模块中找到 Hash 特征:

#![allow(unused)]
fn main() {
pub trait Hash {
    // 必须实现的方法
    fn hash<H>(&self, state: &mut H)
       where H: Hasher;
}
}

你很少会手动实现 Hash,大多数时候你会通过派生来实现它:

#![allow(unused)]
fn main() {
#[derive(Hash)]
struct Person {
    id: u32,
    name: String,
}
}

Eq

HashMap 必须能够比较键的相等性。在处理哈希碰撞时这一点尤为重要——即两个不同的键哈希到相同的值。

你可能疑惑:这不是 PartialEq 特征的作用吗?几乎!但 PartialEq 对于 HashMap 来说不够,因为它不保证自反性,即 a == a 总是 true。例如,浮点数 (f32f64) 实现了 PartialEq,但它们不满足自反性:f32::NAN == f32::NANfalse。自反性对于 HashMap 正确工作至关重要:没有它,你将无法使用插入时的相同键从映射中检索值。

Eq 特征扩展了 PartialEq 并包含了自反性:

#![allow(unused)]
fn main() {
pub trait Eq: PartialEq {
    // 无新增方法
}
}

它是一个标记特征:它不添加任何新方法,只是你向编译器表明在 PartialEq 中实现的等价逻辑是自反性的一种方式。

当你派生 PartialEq 时,也可以自动派生 Eq

#![allow(unused)]
fn main() {
#[derive(PartialEq, Eq)]
struct Person {
    id: u32,
    name: String,
}
}

EqHash 的联系

EqHash 之间存在着隐含的约定:如果两个键相等,它们的哈希值也必须相等。这对于 HashMap 正确工作至关重要。如果你破坏了这个约定,使用 HashMap 时将会得到不合逻辑的结果。