将具有关联值的键存储在哈希 Map 中

我们的最后一个常见集合是哈希映射。类型HashMap<K, V>存储Kto 类型的值V使用哈希 函数,它决定了如何将这些键和值放入内存中。 许多编程语言都支持这种数据结构,但它们通常 使用不同的名称,例如 hashmapobjecthash tabledictionaryassociative array,仅举几例。

当您想通过使用索引来查找数据时,哈希映射非常有用,因为 您可以使用 vectors,但使用可以是任何类型的 key。例如 在游戏中,您可以在哈希图中跟踪每支球队的分数,其中 每个键是团队的名称,值是每个团队的分数。给定一个团队 name 中,您可以检索其 score。

在本节中,我们将介绍哈希映射的基本 API,但还有更多好东西 隐藏在HashMap<K, V>由 Standard 库。 与往常一样,请查看标准库文档以获取更多信息。

创建新的哈希 Map

创建空哈希 map 的一种方法是使用new并使用insert.在示例 8-20 中,我们跟踪了两个团队的分数,他们的 名称为 BlueYellow。蓝队开始时有 10 分,而 黄色球队从 50 开始。

fn main() {
    use std::collections::HashMap;

    let mut scores = HashMap::new();

    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Yellow"), 50);
}

示例 8-20:创建一个新的哈希 map 并插入一些 键和值

请注意,我们首先需要useHashMap从 的 collections 部分 标准库。在我们的三个常见系列中,这个系列最少 经常使用,因此它不包括在引入范围的功能中 自动在 Prelude 中。哈希 map 从 标准库;例如,没有内置的宏来构造它们。

就像向量一样,哈希 map 将其数据存储在堆上。这HashMap具有 类型的键String和 typei32.与向量一样,哈希映射也是 homogeneous:所有 key 必须具有相同的类型,并且所有值 必须具有相同的类型。

访问哈希 Map 中的值

我们可以通过将哈希 map 的键提供给get方法,如示例 8-21 所示。

fn main() {
    use std::collections::HashMap;

    let mut scores = HashMap::new();

    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Yellow"), 50);

    let team_name = String::from("Blue");
    let score = scores.get(&team_name).copied().unwrap_or(0);
}

示例 8-21:访问 Blue 团队的分数 存储在哈希 map 中

这里score将具有与 Blue 团队关联的值,而 result 将为10.这getmethod 返回一个Option<&V>;如果没有 值,get将返回None.这个程序 处理Option通过调用copied要获取Option<i32>而不是Option<&i32>然后unwrap_or设置score如果scores不 具有键的条目。

我们可以像 do 的 vectors,使用for圈:

fn main() {
    use std::collections::HashMap;

    let mut scores = HashMap::new();

    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Yellow"), 50);

    for (key, value) in &scores {
        println!("{key}: {value}");
    }
}

此代码将按任意顺序打印每对:

Yellow: 50
Blue: 10

哈希映射和所有权

对于实现Copytrait 中,例如i32,则会复制值 放入哈希 map 中。对于拥有的值,如String,值将被移动,并且 哈希 map 将是这些值的所有者,如示例 8-22 所示。

fn main() {
    use std::collections::HashMap;

    let field_name = String::from("Favorite color");
    let field_value = String::from("Blue");

    let mut map = HashMap::new();
    map.insert(field_name, field_value);
    // field_name and field_value are invalid at this point, try using them and
    // see what compiler error you get!
}

示例 8-22:显示键和值由 插入后的哈希 map

我们不能使用变量field_namefield_value后 它们已通过对insert.

如果我们将对值的引用插入到哈希 map 中,则值不会被移动 放入哈希 map 中。引用指向的值必须对 at 有效 至少只要哈希 map 有效。我们将在 “验证引用 Lifetimes“部分 第 10 章.

更新 Hash Map

尽管键和值对的数量是可增长的,但每个唯一键都可以 一次只有一个值与之关联(但反之则不然:对于 例如,Blue team 和 Yellow team 都可以具有值10存储在scores哈希映射)。

当您想更改哈希映射中的数据时,您必须决定如何 处理 key 已分配值的情况。您可以将 旧值替换为新值,完全忽略旧值。你可以 保留旧值并忽略新值,仅在 key 还没有值。或者,您可以将旧值和 new 值。让我们看看如何执行这些作!

覆盖值

如果我们将一个键和一个值插入到哈希 map 中,然后插入相同的键 使用不同的值,则与该键关联的值将被替换。 尽管示例 8-23 中的代码调用insert两次,哈希 map 将 仅包含一个键值对,因为我们要为 Blue 插入值 球队的 Key 两次。

fn main() {
    use std::collections::HashMap;

    let mut scores = HashMap::new();

    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Blue"), 25);

    println!("{scores:?}");
}

示例 8-23:将存储的值替换为特定的 钥匙

此代码将打印{"Blue": 25}.的原始值10已经 覆盖。

仅在 key 不存在时添加 key 和 value

检查哈希 map 中是否已经存在特定键是很常见的 替换为值,然后执行以下作:如果 key 确实存在于 哈希 map 时,现有值应保持原样;如果密钥 不存在,请插入它及其值。

哈希 map 有一个特殊的 API 用于此,称为entry那拿你的关键 想要作为参数进行检查。的entrymethod 是枚举 叫Entry这表示可能存在也可能不存在的值。假设 我们想检查 Yellow 团队的 key 是否具有关联的值 与它。如果没有,我们想将值50,对于 蓝队。使用entryAPI 中,代码类似于示例 8-24。

fn main() {
    use std::collections::HashMap;

    let mut scores = HashMap::new();
    scores.insert(String::from("Blue"), 10);

    scores.entry(String::from("Yellow")).or_insert(50);
    scores.entry(String::from("Blue")).or_insert(50);

    println!("{scores:?}");
}

示例 8-24:使用entry方法仅在 if 时插入 键还没有值

or_insertmethod 开启Entry定义为返回对 相应Entrykey (如果该键存在),则为 it(如果不存在) 插入参数作为此键的新值,并返回一个 mutable 引用新值。这种技术比编写 logic 的 logic,此外,它与 Borrow Checker 配合得更好。

运行示例 8-24 中的代码将打印{"Yellow": 50, "Blue": 10}.这 首次调用entry将插入 Yellow 团队的键,其值为50因为 Yellow 团队还没有价值。对entry不会更改哈希 map,因为蓝队已经拥有 价值10.

根据旧值更新值

哈希 map 的另一个常见用例是查找 key 的值,然后 根据旧值更新它。例如,示例 8-25 显示了 计算每个单词在某个文本中出现的次数。我们使用哈希 map 和 单词作为键并增加值以跟踪我们 看到那个词。如果这是我们第一次看到一个单词,我们将首先插入 价值0.

fn main() {
    use std::collections::HashMap;

    let text = "hello world wonderful world";

    let mut map = HashMap::new();

    for word in text.split_whitespace() {
        let count = map.entry(word).or_insert(0);
        *count += 1;
    }

    println!("{map:?}");
}

示例 8-25:使用哈希计算单词的出现次数 存储单词和计数的 map

此代码将打印{"world": 2, "hello": 1, "wonderful": 1}.您可能会看到 以不同顺序打印的相同键值对:回想一下“访问哈希映射中的值”部分,该部分 迭代哈希 map 以任意顺序进行。

split_whitespacemethod 返回子切片的迭代器,以 whitespace 的text.这or_insertmethod 返回一个 mutable 参考 (&mut V) 设置为指定键的值。在这里,我们存储了 mutable 引用的count变量,因此为了分配给该值, 我们必须先取消引用count使用星号 ()。可变的 reference 在*for循环,所以所有这些 更改是安全的,并且是借用规则允许的。

哈希函数

默认情况下,HashMap使用名为 SipHash 的哈希函数,该函数可以提供 抵御涉及哈希的拒绝服务 (DoS) 攻击 表1.这不是最快的哈希算法 可用,但为了更好的安全性而付出了代价 性能是值得的。如果您分析代码并发现默认的 hash 函数太慢了,你可以切换到另一个函数 通过指定不同的 hasher 来执行。哈希器是一种实现BuildHasher特性。我们将在第 10 章讨论 trait 以及如何实现它们。您不一定非得实施 从头开始您自己的哈希器;crates.io 具有其他 Rust 用户共享的库,这些库提供了实现许多 常见的哈希算法。

总结

向量、字符串和哈希映射将提供大量功能 在需要存储、访问和修改数据的程序中是必需的。以下是 您现在应该准备好解决的一些练习:

  1. 给定一个整数列表,使用一个向量并返回中位数(排序后, 中间位置的值)和 mode (出现次数最多的值 经常;哈希映射在这里会有所帮助)。
  2. 将字符串转换为 pig 拉丁语。每个单词的第一个辅音被移动到 单词的结尾 和 ay 被加上去,所以 first 变成了 irst-fay。的话 以元音开头的元音在结尾加上 hayapple 变成 apple-hay)。请记住有关 UTF-8 编码的详细信息!
  3. 使用哈希映射和向量,创建一个文本界面以允许用户添加 员工姓名到公司中的某个部门;例如,“Add Sally to (将 Sally 添加到 工程“或”将 Amir 添加到 Sales”。然后让用户检索所有 部门中的人员或公司中按部门划分的所有人员,已排序 按字母顺序。

标准库 API 文档描述了 vectors、 strings、 哈希图对这些练习很有帮助!

我们正在研究更复杂的程序,在这些程序中,作可能会失败,因此 这是讨论错误处理的最佳时机。我们接下来会这样做!

本文档由官方文档翻译而来,如有差异请以官方英文文档(https://doc.rust-lang.org/)为准