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

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

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

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

创建新的哈希 Map

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

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 并插入一些 键和值

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

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

访问哈希 Map 中的值

我们可以通过向方法提供 hash map 的 key 来从 hash map 中获取一个值,如示例 8-21 所示。get

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 中

此处,将具有与 Blue 团队关联的值,而 result 将为 。该方法返回一个 ;如果没有 值将返回 。这个程序 通过调用来处理 以获取 an 而不是 an ,如果没有,则设置为零 具有键的条目。score10getOption<&V>getNoneOptioncopiedOption<i32>Option<&i32>unwrap_orscorescores

我们可以像 do with 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

哈希映射和所有权

对于实现 trait 的类型(如 ),将复制值 放入哈希 map 中。对于拥有的值(如 ),这些值将被移动,并且 哈希 map 将是这些值的所有者,如示例 8-22 所示。Copyi32String

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_valueinsert

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

更新 Hash Map

尽管键和值对的数量是可增长的,但每个唯一键都可以 一次只有一个值与之关联(但反之则不然:对于 例如,蓝队和黄队都可以将值存储在哈希 map 中)。10scores

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

覆盖值

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

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 时,现有值应保持原样;如果密钥 不存在,请插入它及其值。

哈希映射有一个特殊的 API 用于此,称为 API,它接受密钥 you 想要作为参数进行检查。该方法的返回值是一个枚举 called 表示可能存在也可能不存在的值。假设 我们想检查 Yellow 团队的 key 是否具有关联的值 与它。如果没有,我们想插入值 ,对于 蓝队。使用 API,代码如图 8-24 所示。entryentryEntry50entry

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 方法仅在 键还没有值

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

运行示例 8-24 中的代码将打印 .这 第一次调用 to 将插入 Yellow 团队的 key 和 value,因为 Yellow 团队还没有值。第二次调用 不会更改哈希 map,因为 Blue 团队已经拥有 价值。{"Yellow": 50, "Blue": 10}entry50entry10

根据旧值更新值

哈希 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

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

该方法返回子切片的迭代器,以 空格,值为 。该方法返回一个 mutable reference () 设置为指定键的值。在这里,我们存储了 可变引用,因此为了将该值赋值为 我们必须首先使用星号 () 取消引用。可变的 reference 在循环结束时超出范围,因此所有这些 更改是安全的,并且是借用规则允许的。split_whitespacetextor_insert&mut Vcountcount*for

哈希函数

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

总结

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

  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/)为准