开发环境
- Windows 10
- Rust 1.66.0
- VS Code 1.74.2
项目工程
这里继续沿用上次工程rust-demo
在哈希图中存储带有关联值的键
我们常见的集合中的最后一个是哈希映射。HashMap<K, V>类型使用散列函数存储K类型的键到V类型的值的映射,这决定了它如何将这些键和值放入内存。许多编程语言都支持这种数据结构,但它们通常使用不同的名称,如哈希、Map、对象、哈希表、字典或关联数组,仅举几例。
当你不像使用向量那样使用索引来查找数据,而是使用一个可以是任何类型的键来查找时,哈希映射就很有用。例如,在一个游戏中,你可以在一个哈希映射中记录每个球队的得分,其中每个键是一个球队的名字,值是每个球队的得分。给定一个球队的名字,你可以检索它的分数。
我们将在本节中介绍哈希映射的基本API,但还有很多好东西隐藏在标准库对HashMap<K, V>定义的函数中。一如既往,请查看标准库文档以了解更多信息。
创建新的哈希映射
创建空哈希映射的一种方法是使用new,用insert添加元素。在清下例中,我们要记录两支球队的分数,他们的名字是蓝队和黄队。蓝队以10分开始,而黄队以50分开始。
fn main() {
use std::collections::HashMap; // HashMap
let mut scores = HashMap::new(); // 创建HashMap对象
scores.insert(String::from("Blue"), 10); // 存储键,值
scores.insert(String::from("Yellow"), 50);
}
请注意,我们需要首先use标准库中集合部分的HashMap。在我们的三个常用集合中,这个集合是最不常用的,所以它不包括在前奏中自动带入范围的特性中。哈希映射在标准库中得到的支持也比较少;例如,没有内置的宏来构造它们。
就像向量一样,哈希映射将其数据存储在堆上。这个HashMap的键是String类型,值是i32类型。像向量一样,哈希映射是同质的:所有的键必须具有相同的类型,所有的值必须具有相同的类型。
访问哈希映射中的值
我们可以通过向get方法提供键来从哈希图中获取一个值。如下例所示
fn main() {
use std::collections::HashMap; // 使用HashMap
let mut scores = HashMap::new(); // 创建HashMap对象
scores.insert(String::from("Blue"), 10); // 存储键,值
scores.insert(String::from("Yellow"), 50);
let team_name = String::from("Blue"); // 访问HashMap中的键
println!("team_name = {}", team_name);
let score = scores.get(&team_name).copied().unwrap_or(0);
println!("score = {}", score);
}
编译,运行
cargo run
在这里,score将有与蓝队相关的值,结果将是10。get方法返回一个Option<&V>;如果哈希映射中没有该键的值,get将返回None。这个程序通过调用copied得到一个Option<i32>而不是Option<&i32>来处理Option,如果scores中没有该键的条目,则unwrap_or将score设为0。
我们可以用类似于处理向量的方式来迭代哈希映射中的每个键/值对,使用for循环。
fn main() {
use std::collections::HashMap; // 使用HashMap
let mut scores = HashMap::new(); // 创建HashMap对象
scores.insert(String::from("Blue"), 10); // 存储键,值
scores.insert(String::from("Yellow"), 50);
for (key, value) in &scores { // 遍历HashMap,打印键,值
println!("{key}: {value}");
}
}
运行
cargo run
哈希映射和所有权
对于实现了Copy特性的类型,如i32,值被复制到哈希图中。对于像String这样的自有值,这些值将被移动,哈希图将成为这些值的所有者,如下例所示。
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); // insert方法
}
在调用insert将变量field_name和field_value移入哈希映射后,我们无法使用这些变量。
如果我们在哈希映射中插入对数值的引用,这些数值就不会被移到哈希图映射中。引用所指向的值必须至少在哈希映射有效的时间内是有效的。
更新哈希映射
虽然键和值对的数量是可以增长的,但每个独特的键在同一时间只能有一个与之相关的值(但反之则不然:例如,蓝队和黄队都可以在scores哈希映射中存储值10)。
当你想改变哈希映射中的数据时,你必须决定如何处理一个键已经分配了一个值的情况。你可以用新值替换旧值,完全不考虑旧值。你可以保留旧值而忽略新值,只在键还没有值的情况下添加新值。或者你可以把旧值和新值结合起来。让我们来看看如何做这些事情吧!
覆盖值
如果我们在哈希映射中插入一个键和一个值,然后用不同的值插入同一个键,那么与该键相关的值将被替换。尽管下例中的代码调用了两次insert,但哈希映射将只包含一个键/值对,因为我们两次都是插入蓝队的键的值。
fn main() {
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Blue"), 25); // 覆盖了键为Blue的值
println!("{:?}", scores);
}
运行
cargo run
这段代码将打印{"Blue":25}。原来的数值10已经被覆盖了。
仅当键不存在时才添加键和值
常见的做法是检查一个特定的键是否已经存在于哈希映射中的一个值,然后采取以下行动:如果该键确实存在于哈希映射中,现有的值应该保持原状。如果该键不存在,则插入该键和它的一个值。
哈希映射为此有一个特殊的API,叫做entry,它把你想检查的键作为一个参数。entry方法的返回值是一个叫做Entry的枚举,代表一个可能存在或不存在的值。比方说,我们想检查黄队的钥匙是否有一个与之相关的值。如果没有,我们要插入值50,对蓝队也是如此。使用entryAPI,代码看起来如下例。
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); // entry接口
scores.entry(String::from("Blue")).or_insert(50);
println!("{:?}", scores);
}
运行
cargo run
Entry上的or_insert方法被定义为:如果该键存在,则返回对应的Entry键的值的可变引用;如果不存在,则插入参数作为该键的新值并返回新值的可变引用。这种技术比我们自己写逻辑要干净得多,此外,与借贷检查器的配合也更加默契。
运行上例中的代码将打印{"黄": 50, "Blue": 10}。对entry的第一次调用将插入黄队的键,值为50,因为黄队还没有一个值。第二次调用entry不会改变哈希图,因为蓝队已经有了值10。
基于旧值更新一个数值
哈希映射的另一个常见用例是查询一个键的值,然后根据旧值进行更新。例如,下例中显示了计算每个词在一些文本中出现多少次的代码。我们使用一个以单词为键的哈希映射,并递增其值以记录我们看到该单词的次数。如果这是我们第一次看到一个词,我们将首先插入值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); // entry接口
*count += 1;
}
println!("{:?}", map);
}
运行
cargo run
这段代码将打印{"world"。2, "hello": 1, "wonderful": 1}. 你可能会看到相同的键/值对以不同的顺序打印出来:回顾一下 "访问哈希映射中的值 "部分,在哈希映射上的迭代是以任意的顺序进行的。
split_whitespace方法返回text中值的子片的迭代器,子片之间用空格分隔。or_insert方法返回一个对指定键的值的可变引用(&mut V)。在这里,我们把这个易变的引用存储在count变量中,所以为了赋值给这个值,我们必须先用星号(*)解除对count的引用。可变引用在for循环结束时退出作用域,所以所有这些变化都是安全的,也是借用规则所允许的。
哈希函数
默认情况下,HashMap使用一个名为SipHash的散列函数,可以抵抗涉及散列表的拒绝服务(DoS)攻击1。这不是现有的最快的散列算法,但是随着性能的下降而带来的更好的安全性的折衷是值得的。如果你对你的代码进行剖析,发现默认的哈希函数对你的目的来说太慢了,你可以通过指定一个不同的哈希函数来切换到另一个函数。哈希函数是一个实现了BuildHasher特性的类型。我们将在后续讨论特质和如何实现它们的问题。你不一定要从头开始实现你自己的哈希器;crates.io有其他Rust用户共享的库,提供了实现许多常见哈希算法的哈希器。
总结
当你需要存储、访问和修改数据时,Vectors,Strings和HashMap将在程序中提供大量的必要功能。下面是一些你现在应该有能力解决的练习。
- 给定一个整数列表,使用一个向量并返回列表的中位数(当排序时,位于中间位置的值)和模式(出现频率最高的值;哈希图在这里会有帮助)。
- 将字符串转换为猪拉丁语。每个词的第一个辅音被移到词尾,并加上 "ay",所以 "first "变成 "irst-fay"。以元音开头的单词则在词尾加上 "hay"("苹果 "变成 "apple-hay")。牢记关于UTF-8编码的细节!
- 使用哈希映射和向量,创建一个文本界面,让用户将员工姓名添加到公司的某个部门。例如,"将莎莉添加到工程部 "或 "将阿米尔添加到销售部"。然后让用户检索一个部门所有人员的列表,或按部门检索公司所有人员的列表,按字母排序。
标准库的API文档描述了向量、字符串和哈希映射的方法,这些方法对这些练习很有帮助!
本章重点
- 哈希映射的概念
- 创建哈希映射,存储键和值
- 访问哈希映射的键和值
- 哈希映射的所有权
- 更新哈希映射:覆盖,当键不存在添加键和值,基于旧值更新数值