将具有关联值的键存储在哈希 Map 中
我们的最后一个常见集合是哈希映射。类型HashMap<K, V>
存储K
to 类型的值V
使用哈希
函数,它决定了如何将这些键和值放入内存中。
许多编程语言都支持这种数据结构,但它们通常
使用不同的名称,例如 hash、map、object、hash table、dictionary 或 associative array,仅举几例。
当您想通过使用索引来查找数据时,哈希映射非常有用,因为 您可以使用 vectors,但使用可以是任何类型的 key。例如 在游戏中,您可以在哈希图中跟踪每支球队的分数,其中 每个键是团队的名称,值是每个团队的分数。给定一个团队 name 中,您可以检索其 score。
在本节中,我们将介绍哈希映射的基本 API,但还有更多好东西
隐藏在HashMap<K, V>
由 Standard 库。
与往常一样,请查看标准库文档以获取更多信息。
创建新的哈希 Map
创建空哈希 map 的一种方法是使用new
并使用insert
.在示例 8-20 中,我们跟踪了两个团队的分数,他们的
名称为 Blue 和 Yellow。蓝队开始时有 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 并插入一些 键和值
请注意,我们首先需要use
这HashMap
从 的 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
.这get
method 返回一个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
哈希映射和所有权
对于实现Copy
trait 中,例如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_name
和field_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
那拿你的关键
想要作为参数进行检查。的entry
method 是枚举
叫Entry
这表示可能存在也可能不存在的值。假设
我们想检查 Yellow 团队的 key 是否具有关联的值
与它。如果没有,我们想将值50
,对于
蓝队。使用entry
API 中,代码类似于示例 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_insert
method 开启Entry
定义为返回对
相应Entry
key (如果该键存在),则为 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_whitespace
method 返回子切片的迭代器,以
whitespace 的text
.这or_insert
method 返回一个 mutable
参考 (&mut V
) 设置为指定键的值。在这里,我们存储了
mutable 引用的count
变量,因此为了分配给该值,
我们必须先取消引用count
使用星号 ()。可变的
reference 在*
for
循环,所以所有这些
更改是安全的,并且是借用规则允许的。
哈希函数
默认情况下,HashMap
使用名为 SipHash 的哈希函数,该函数可以提供
抵御涉及哈希的拒绝服务 (DoS) 攻击
表1.这不是最快的哈希算法
可用,但为了更好的安全性而付出了代价
性能是值得的。如果您分析代码并发现默认的
hash 函数太慢了,你可以切换到另一个函数
通过指定不同的 hasher 来执行。哈希器是一种实现BuildHasher
特性。我们将在第 10 章讨论 trait 以及如何实现它们。您不一定非得实施
从头开始您自己的哈希器;crates.io 具有其他 Rust 用户共享的库,这些库提供了实现许多
常见的哈希算法。
总结
向量、字符串和哈希映射将提供大量功能 在需要存储、访问和修改数据的程序中是必需的。以下是 您现在应该准备好解决的一些练习:
- 给定一个整数列表,使用一个向量并返回中位数(排序后, 中间位置的值)和 mode (出现次数最多的值 经常;哈希映射在这里会有所帮助)。
- 将字符串转换为 pig 拉丁语。每个单词的第一个辅音被移动到 单词的结尾 和 ay 被加上去,所以 first 变成了 irst-fay。的话 以元音开头的元音在结尾加上 hay (apple 变成 apple-hay)。请记住有关 UTF-8 编码的详细信息!
- 使用哈希映射和向量,创建一个文本界面以允许用户添加 员工姓名到公司中的某个部门;例如,“Add Sally to (将 Sally 添加到 工程“或”将 Amir 添加到 Sales”。然后让用户检索所有 部门中的人员或公司中按部门划分的所有人员,已排序 按字母顺序。
标准库 API 文档描述了 vectors、 strings、 哈希图对这些练习很有帮助!
我们正在研究更复杂的程序,在这些程序中,作可能会失败,因此 这是讨论错误处理的最佳时机。我们接下来会这样做!
本文档由官方文档翻译而来,如有差异请以官方英文文档(https://doc.rust-lang.org/)为准