首页
/ Rustlings 11_hashmaps 练习解析:用 HashMap 掌握 Rust 键值映射的实战用法

Rustlings 11_hashmaps 练习解析:用 HashMap 掌握 Rust 键值映射的实战用法

2026-09-04 20:30:48作者:薛曦旖Francesca

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);

这里有三个值得注意的细节:

  1. HashMap 需要从 std::collections 中引入,它不在预编译作用域里;
  2. 键类型是 String——任何用作 HashMap 键的类型都必须实现 HashEq trait,String 天然满足;
  3. 后续要对 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(或手动实现)HashPartialEq + 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_fruitslen() 断言种类数不少于 5;greater_than_eleven_fruitsvalues().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 同样实现了 HashEq,可以作键;
  • 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_scorescontains_key 确认 6 支球队全部出现在表中;validate_team_score_1 断言 England 的 goals_scored == 6goals_conceded == 4validate_team_score_2 断言 Spain 的 goals_scored == 0goals_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 中“按键查找并安全更新”这一最常见模式的标准写法,也为后续更复杂的集合操作打下了基础。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
903
1.82 K
docsdocs
暂无描述
Markdown
888
5.78 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.51 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384
flutter_flutterflutter_flutter
本仓库是 Flutter SDK 与 Flutter Engine 的 OpenHarmony 适配版本,由 CPF-Flutter 团队维护。开发者可使用熟悉的 Flutter 技术栈开发 OpenHarmony 应用,3.35.7 及以后的适配版本可基于本仓库源码构建支持 OpenHarmony 的 Flutter Engine。
Dart
1.17 K
341