Rustlings 11_hashmaps 练习解析:用 HashMap 掌握 Rust 键值映射的实战用法
Rustlings 的 11_hashmaps 练习组(exercises/11_hashmaps/README.md)带你完成 Rust 标准库中 HashMap 数据结构的完整入门:从声明与插入键值对,到 Entry API 的条件插入,再到用 HashMap 做分组聚合统计。本文逐题拆解这 3 道练习的题目要求、核心 API 与官方参考解法,并结合 solutions/11_hashmaps/ 中的参考答案说明每个关键步骤背后的实现原理,帮助你系统掌握 HashMap 的日常用法。
什么是 Hash Map
按 exercises/11_hashmaps/README.md 的定义,Hash map(哈希表)允许你将一个值与某个特定的键关联起来。它在其他语言中有不同的叫法:
- C++ 中称为 unordered map(无序映射);
- Python 中称为 dictionary(字典);
- 其他语言中常称 associative array(关联数组)。
README 同时指出:Hash map 正是前面讲 Vec 时提到的“另一种数据结构”。这一伏笔可以回溯到 exercises/05_vecs/README.md——其中明确写道“我们将稍后再谈另一种有用的数据结构,即哈希表”。两者共同构成了 Rust 中最常用的两大集合类型:Vec 存储有序的值列表,HashMap 存储键到值的映射。
按 exercises/README.md 中的练习与书章节映射表,hashmaps 对应 Rust 官方书 §8.3(Storing Keys with Associated Values in Hash Maps),这也是本练习组 Further information 一节所指向的章节。
练习位置与运行方式
本练习组位于 exercises/11_hashmaps/ 目录,包含 3 个练习文件:
| 文件 | 主题 |
|---|---|
| hashmaps1.rs | 声明 HashMap 并用 insert 插入键值对 |
| hashmaps2.rs | Entry API:entry(...).or_insert(...) 条件插入 |
| hashmaps3.rs | Entry API:entry(...).or_default() 聚合统计 |
根据 website/content/usage/index.md 的说明,初始化 Rustlings 后直接运行 rustlings 命令即可进入 watch 模式,它会按预定顺序带你逐题完成练习,并在你每次修改 exercises/ 目录下的文件后自动重新运行当前练习。每个练习文件中搜索 TODO 即可定位需要修改的位置;进入 watch 模式后按 h 可以获取提示。每个练习自带 #[cfg(test)] 测试模块,练习完成的判据就是这些测试全部通过。
练习一:声明 HashMap 并插入键值对
exercises/11_hashmaps/hashmaps1.rs 的题面要求:定义一个“水果篮子”HashMap,键是水果名称(字符串),值是该种水果的数量。要求放入至少 3 种不同的水果,且水果总数至少为 5。题目已替你插入 2 根香蕉:
use std::collections::HashMap;
fn fruit_basket() -> HashMap<String, u32> {
// TODO: Declare the hash map.
// let mut basket =
// Two bananas are already given for you :)
basket.insert(String::from("banana"), 2);
// TODO: Put more fruits in your basket.
basket
}
官方参考解法(见 solutions/11_hashmaps/hashmaps1.rs)只补了两处:
let mut basket = HashMap::new(); // 声明空 HashMap,必须加 mut
basket.insert(String::from("apple"), 3);
basket.insert(String::from("mango"), 1);
这里有三个值得注意的细节:
HashMap需要从std::collections中引入,它不在预编译作用域里;- 键类型是
String——任何用作 HashMap 键的类型都必须实现Hash和Eqtrait,String天然满足; - 后续要对 map 做插入操作,绑定必须声明为
mut。
该练习的两个测试分别用 basket.len() >= 3 校验种类数、用 basket.values().sum::<u32>() >= 5 校验数量总和——values() 返回所有值的迭代器,sum::<u32>() 是迭代器上的累加方法,这两个 API 在本组练习中反复出现。
练习二:Entry API 与自定义类型作为键
exercises/11_hashmaps/hashmaps2.rs 把场景升级为“收集水果做果糕”:篮子里已有 Apple(4)、Mango(2)、Lychee(5)三种水果,你必须在不改动已有水果数量的前提下补充水果,使每种水果至少 1 个、总数超过 11。
这道题的第一个考点是把自定义枚举用作 HashMap 的键。题面已给出:
#[derive(Hash, PartialEq, Eq, Debug)]
enum Fruit {
Apple,
Banana,
Mango,
Lychee,
Pineapple,
}
要让一个类型能当 HashMap 的键,它必须 derive(或手动实现)Hash 与 PartialEq + Eq trait——这是 Rust 对“键的可哈希性”的编译期约束。Debug 则用于测试中的格式化输出。
第二个考点是 Entry API。循环需要“若该水果不在篮子里则插入一个”:
for fruit in fruit_kinds {
// TODO: Insert new fruits if they are not already present in the
// basket. Note that you are not allowed to put any type of fruit that's
// already present!
}
参考解法(solutions/11_hashmaps/hashmaps2.rs)一行搞定:
basket.entry(fruit).or_insert(5);
entry(fruit) 返回一个 Entry,表示该键“已存在”或“可插入”两种状态;or_insert(5) 表示:键已存在则保留原值不动,键不存在则插入 5。这恰好满足题目“不允许再插入已有水果”的约束——对比之下,直接 insert 会覆盖已有键的值,从而破坏 Apple=4、Mango=2、Lychee=5 的初始数据。
该练习的 4 个测试覆盖了所有约束:test_given_fruits_are_not_modified 断言原有三种水果数量未变;at_least_five_types_of_fruits 用 len() 断言种类数不少于 5;greater_than_eleven_fruits 用 values().sum::<u32>() 断言总数超过 11;all_fruit_types_in_basket 则用 basket.get(&fruit_kind) 逐一确认五种水果都存在且数量大于 0。另外注意测试辅助函数用 HashMap::from_iter(content) 从键值元组切片构造初始 map,这是另一个常用的构造方式。
练习三:用 or_default 聚合统计足球比分
exercises/11_hashmaps/hashmaps3.rs 是本组最综合的一道题:给定一个多行字符串,每行格式为 "<队伍1>,<队伍2>,<队伍1进球数>,<队伍2进球数>",要求构建一张积分表,记录每支球队的总进球数(goals_scored)和总失球数(goals_conceded)。
题目已搭好骨架:值类型是一个带 #[derive(Default)] 的结构体,new() 的字段默认值就是 0:
#[derive(Default)]
struct TeamScores {
goals_scored: u8,
goals_conceded: u8,
}
fn build_score_table(results: &str) -> HashMap<&str, TeamScores> {
let mut scores = HashMap::<&str, TeamScores>::new();
// ...
}
注意这里有两个细节:
- 键类型是
&str而非String,因为队名直接来自输入字符串的切片,无需拥有所有权——&str同样实现了Hash和Eq,可以作键; HashMap::<&str, TeamScores>::new()用了 turbofish 语法显式标注类型参数,避免类型推断失败。
解析循环中,题面已经用 line.split(',') 和 next() 取出四个字段,并用 parse().unwrap() 把进球数解析为 u8(题面注释说明:因为还没学到错误处理,所以先用 unwrap)。你需要补全 TODO 处的聚合逻辑。参考解法(solutions/11_hashmaps/hashmaps3.rs)如下:
// Insert the default with zeros if a team doesn't exist yet.
let team_1 = scores.entry(team_1_name).or_default();
// Update the values.
team_1.goals_scored += team_1_score;
team_1.goals_conceded += team_2_score;
// Similarly for the second team.
let team_2 = scores.entry(team_2_name).or_default();
team_2.goals_scored += team_2_score;
team_2.goals_conceded += team_1_score;
这里用的是 Entry API 的另一个方法:or_default()。它的语义是:键已存在则返回对现有值的可变引用,键不存在则先插入 TeamScores::default()(即两个字段均为 0 的值)再返回其可变引用。正因为返回的是 &mut 引用,后续才能直接对 goals_scored / goals_conceded 做累加。
这道题还隐含一个数据关系:队伍 1 的进球就是队伍 2 的失球,反之亦然,所以每处理一行比赛结果,两支球队的结构体都要更新一次。测试数据包含 5 场比赛、6 支球队:
England,France,4,2
France,Italy,3,1
Poland,Spain,2,0
Germany,England,2,1
England,Spain,1,0
对应的 3 个测试验证:build_scores 用 contains_key 确认 6 支球队全部出现在表中;validate_team_score_1 断言 England 的 goals_scored == 6、goals_conceded == 4;validate_team_score_2 断言 Spain 的 goals_scored == 0、goals_conceded == 3。
核心 API 小结
贯穿这 3 道练习,HashMap 的关键 API 可以归纳如下(均可在当前仓库的练习与参考解法中找到实际用例):
| API | 作用 | 用例出处 |
|---|---|---|
HashMap::new() |
创建空 map(可用 turbofish 标注类型参数) | hashmaps1 解法、hashmaps3 题面 |
HashMap::from_iter(iter) |
从键值对迭代器构造 map | hashmaps2 测试 |
map.insert(key, value) |
插入/覆盖一个键值对 | hashmaps1 解法 |
map.get(&key) |
按键查找,返回 Option<&V> |
hashmaps2 测试 |
map.contains_key(&key) |
判断键是否存在 | hashmaps3 测试 |
map.len() / map.values() |
元素个数 / 所有值的迭代器 | 各练习的测试断言 |
map.entry(key).or_insert(v) |
键不存在时才插入 v | hashmaps2 解法 |
map.entry(key).or_default() |
键不存在时插入 Default 值并返回可变引用 |
hashmaps3 解法 |
学习建议
完成本组练习时,建议按以下顺序自测:先在本地把三个 TODO 都补全并让 watch 模式的测试全部变绿,再对照 solutions/11_hashmaps/ 下的三个参考解法,重点关注 entry().or_insert() 与 entry().or_default() 的行为差异——前者需要你显式给出插入的默认值,后者要求值类型实现 Default,而返回的可变引用让你可以就地累加更新。理解这两个方法后,你就掌握了 Rust 中“按键查找并安全更新”这一最常见模式的标准写法,也为后续更复杂的集合操作打下了基础。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00