- 文档
- 教程
【免费下载链接】rust-by-example
Learn Rust with examples (Live code editor included)
HashMap是 Rust 标准库中最常用的键值存储容器,它以哈希表为底层实现,允许以任意实现Eq与Hashtrait 的类型(如布尔值、整数、字符串)作为键来存取数据。本文以 rust-by-example 仓库中 HashMap 章节 为主体,结合其下的 自定义键类型 与 HashSet 章节,完整讲解 HashMap 的创建、增删查改、迭代与自定义键实战,并延伸介绍基于 HashMap 实现的去重集合HashSet及其四大集合运算,帮助你掌握在 Rust 项目中正确使用哈希容器的完整方法。
HashMap:以键取值,而不是以整数索引取值
向量(Vec)通过整数下标存取元素,而HashMap通过键(key)来存取值(value)。这是两者最本质的区别:
Vec的下标是连续的整数,元素在内存中按顺序排列;HashMap的键可以是布尔值、整数、字符串,或者任何实现了Eq和Hash两个 trait 的类型。
与向量一样,HashMap也是可增长的(growable);不同的是,HashMap在拥有多余空间时还可以自动收缩(shrink),从而更高效地利用内存。这一点在标准库文档中亦有说明,关于底层实现可进一步查阅 Rust 官方std::collections文档。
在 Rust By Example 的目录结构 中,HashMap 位于 标准库类型章节 之下,紧随Box、Vec、String等标准容器之后,与其并列的子章节还包括 自定义键类型 与 HashSet。
键的约束:Eq+Hash
HashMap之所以要求键实现Eq和Hash,是因为哈希表的工作方式:
Hash:把键映射为一个哈希值,用于快速定位桶(bucket)的位置;Eq:在哈希值相同的碰撞场景下,用相等性判断确认两个键是否真的是同一个键。
本仓库原文明确指出:HashMap的键可以是布尔值、整数、字符串,或任何实现了Eq和Hashtrait 的类型,并提示"更多内容见下一节"(即 自定义键类型)。
创建 HashMap:new与with_capacity
创建HashMap有两种典型方式:
HashMap::new():使用默认初始容量创建,官方推荐使用;HashMap::with_capacity(uint):以指定容量(usize类型)创建,适合在预先知道元素规模时使用,可减少扩容带来的重哈希开销。
两者的共同点是都返回一个可增长的HashMap。与Vec的 vec! 宏与动态扩容机制 类似,HashMap也采用"容量-长度"管理:当元素数量逼近容量时触发扩容,with_capacity正是通过提前分配空间来摊平这类开销。
实战示例:用 HashMap 实现通讯录
下面这段完整示例来自 src/std/hash.md,实现了按人名存储电话号码并拨打的通讯录逻辑:
use std::collections::HashMap; fn call(number: &str) -> &str { match number { "798-1364" => "We're sorry, the call cannot be completed as dialed. Please hang up and try again.", "645-7689" => "Hello, this is Mr. Awesome's Pizza. My name is Fred. What can I get for you today?", _ => "Hi! Who is this again?" } } fn main() { let mut contacts = HashMap::new(); contacts.insert("Daniel", "798-1364"); contacts.insert("Ashley", "645-7689"); contacts.insert("Katie", "435-8291"); contacts.insert("Robert", "956-1745"); // Takes a reference and returns Option<&V> match contacts.get(&"Daniel") { Some(&number) => println!("Calling Daniel: {}", call(number)), _ => println!("Don't have Daniel's number."), } // `HashMap::insert()` returns `None` // if the inserted value is new, `Some(value)` otherwise contacts.insert("Daniel", "164-6743"); match contacts.get(&"Ashley") { Some(&number) => println!("Calling Ashley: {}", call(number)), _ => println!("Don't have Ashley's number."), } contacts.remove(&"Ashley"); // `HashMap::iter()` returns an iterator that yields // (&'a key, &'a value) pairs in arbitrary order. for (contact, &number) in contacts.iter() { println!("Calling {}: {}", contact, call(number)); } }这段代码涵盖了HashMap最核心的四个操作,值得逐点拆解:
get:只取引用,返回Option<&V>
contacts.get(&"Daniel")接收的是键的引用,返回Option<&V>:
- 键存在时返回
Some(&value); - 键不存在时返回
None。
因为返回的是引用而非所有权,所以示例中用match+ 解引用模式Some(&number)把&str解出来传给call。
insert:插入并返回旧值
insert的返回值揭示了键是否已存在:
- 插入的是一个新键时,返回
None; - 插入的键已存在时,返回
Some(旧值)(旧值被新值替换)。
示例中第二次contacts.insert("Daniel", "164-6743")正是因为此前已存在Daniel键,所以会覆盖旧号码——这正是"以键取值"语义的体现。
remove:按键删除
contacts.remove(&"Ashley")接收键的引用,将Ashley及其号码一并从表中移除。此后若再对该键get,将得到None。
iter:按任意顺序遍历
contacts.iter()返回一个迭代器,逐个产出(&'a K, &'a V)形式的键值引用对,顺序是任意的(哈希表不保证插入顺序)。示例中通过for (contact, &number)同时解构出键和值:
for (contact, &number) in contacts.iter() { println!("Calling {}: {}", contact, call(number)); }对于需要保证插入顺序的场景,可以参考仓库中提到的替代方案(如BTreeSet/BTreeMap等有序容器)。
常用操作速查
综合原文档与标准库行为,HashMap的常用操作可归纳如下:
| 方法 | 签名要点 | 行为与返回值 |
|---|---|---|
insert(k, v) | 传入键值 | 新键返回None,已存在返回Some(旧值) |
get(&k) | 传键引用 | 返回Option<&V> |
remove(&k) | 传键引用 | 移除并返回Option<V>(被移除的值) |
iter() | 借出 | 按任意顺序产出(&K, &V)对 |
contains_key(&k) | 传键引用 | 返回bool,判断键是否存在 |
len() | 借出 | 返回当前存储的键值对数量 |
with_capacity(n) | 传入容量 | 以指定初始容量创建 |
需要说明的是:HashMap的迭代顺序任意,因此依赖顺序的展示应先收集排序;其容量管理(增长与收缩)由标准库自动完成,开发者一般只需关注with_capacity的初始预留。
自定义/替代键类型:不限于字符串和整数
原文档明确指出,任何实现Eq与Hash的类型都可以作为HashMap的键,详见 自定义键类型。可用的键类型包括:
bool:可行但意义有限(只有两个可能的键);int、uint及所有整型变体;String和&str:实用技巧——可以用String作键、用&str调用.get()(String与&str的哈希/相等实现兼容,从而避免在查找时分配新的String)。
浮点数为什么不能作键?
f32和f64没有实现Hash。原因正如原文档所述:浮点数存在精度误差(floating-point precision errors),直接以浮点值作为哈希键会极其容易出错——两个在数学上"相等"的浮点数可能因为舍入差异而哈希不同,或反之。
容器类型的可哈希性
所有集合类,只要其内部元素类型分别实现了Eq和Hash,则集合本身也会实现Eq和Hash。例如Vec<T>在T实现Hash时也会实现Hash,因此Vec<T>这类复合结构也能成为键。
一行代码让自定义类型成为键
对自定义类型而言,只需一行派生:
#[derive(PartialEq, Eq, Hash)]编译器会为你生成完整的实现(其中Eq要求类型先实现PartialEq,所以两者需要一起派生)。这一机制与仓库 derive 章节 中"编译器可通过#[derive]为部分 trait 提供基础实现"的描述一致——可派生的 trait 包括Eq、PartialEq、Clone、Copy、Hash、Default、Debug等。如果需要更精细的控制,也可以手写Eq和/或Hash的实现。
完整实战:用 struct 作为键的登录系统
下面这段来自 alt_key_types.md 的示例,演示了如何用自定义struct作为键,构建一个简单的用户登录校验系统:
use std::collections::HashMap; // Eq requires that you derive PartialEq on the type. #[derive(PartialEq, Eq, Hash)] struct Account<'a>{ username: &'a str, password: &'a str, } struct AccountInfo<'a>{ name: &'a str, email: &'a str, } type Accounts<'a> = HashMap<Account<'a>, AccountInfo<'a>>; fn try_logon<'a>(accounts: &Accounts<'a>, username: &'a str, password: &'a str){ println!("Username: {}", username); println!("Password: {}", password); println!("Attempting logon..."); let logon = Account { username, password, }; match accounts.get(&logon) { Some(account_info) => { println!("Successful logon!"); println!("Name: {}", account_info.name); println!("Email: {}", account_info.email); }, _ => println!("Login failed!"), } } fn main(){ let mut accounts: Accounts = HashMap::new(); let account = Account { username: "j.everyman", password: "password123", }; let account_info = AccountInfo { name: "John Everyman", email: "j.everyman@email.com", }; accounts.insert(account, account_info); try_logon(&accounts, "j.everyman", "psasword123"); try_logon(&accounts, "j.everyman", "password123"); }这个例子的核心价值在于:
- 组合键:
Account { username, password }作为一个整体参与哈希与相等比较,登录时构造同构的临时Account去查询; - 类型别名:
type Accounts<'a> = HashMap<Account<'a>, AccountInfo<'a>>让复杂签名变得可读; - 两次调用对比:第一次密码拼错(
psasword123)查询失败,第二次密码正确(password123)查询成功,直观展示get基于Eq精确匹配的语义。
HashSet:只关心键的去重集合
如果说HashMap存储键值对,那么HashSet就是"只关心键、不关心值"的集合。正如 HashSet 章节 所说:HashSet<T>实质上只是HashMap<T, ()>的包装——值类型被占位为单元类型()。
这带来的核心契约是:HashSet保证集合内没有重复元素,这是任何集合(set)类型都要满足的约定。当插入一个已存在的值(新旧值相等且哈希相同)时,新值会替换旧值。
"为什么不用Vec存键?"——因为去重是HashSet的天然能力,你无需自己写contains检查,它特别适合"每样东西只保留一份"或"判断是否已拥有某物"的场景。仓库原文同时提示,Rust 还提供有序替代实现BTreeSet。
四大集合运算
HashSet有四种主要操作,全部返回迭代器(需配合.collect()收集为Vec等容器):
| 操作 | 语义 |
|---|---|
union | 两个集合中所有不重复元素(并集) |
difference | 只在第一个集合、不在第二个集合的元素(差集) |
intersection | 同时出现在两个集合中的元素(交集) |
symmetric_difference | 出现在其中一个集合、但不同时出现在两个集合的元素(对称差) |
完整示例:集合运算演练
下面的示例改编自标准库文档,完整演示上述四种运算(摘自 hashset.md):
use std::collections::HashSet; fn main() { let mut a: HashSet<i32> = vec![1i32, 2, 3].into_iter().collect(); let mut b: HashSet<i32> = vec![2i32, 3, 4].into_iter().collect(); assert!(a.insert(4)); assert!(a.contains(&4)); // `HashSet::insert()` returns false if // there was a value already present. assert!(b.insert(4), "Value 4 is already in set B!"); // FIXME ^ Comment out this line b.insert(5); // If a collection's element type implements `Debug`, // then the collection implements `Debug`. // It usually prints its elements in the format `[elem1, elem2, ...]` println!("A: {:?}", a); println!("B: {:?}", b); // Print [1, 2, 3, 4, 5] in arbitrary order println!("Union: {:?}", a.union(&b).collect::<Vec<&i32>>()); // This should print [1] println!("Difference: {:?}", a.difference(&b).collect::<Vec<&i32>>()); // Print [2, 3, 4] in arbitrary order. println!("Intersection: {:?}", a.intersection(&b).collect::<Vec<&i32>>()); // Print [1, 5] println!("Symmetric Difference: {:?}", a.symmetric_difference(&b).collect::<Vec<&i32>>()); }这个例子值得注意的细节:
- 从迭代器收集:
vec![1i32, 2, 3].into_iter().collect()演示了Vec到HashSet的经典转换(collect依赖目标类型推断,因此a、b必须显式标注HashSet<i32>); - insert 的返回值:
a.insert(4)因 4 是新元素返回true;b.insert(4)因 4 已存在返回false,示例中用assert!演示了这一行为(注释掉该行即可修复); - Debug 输出:只要元素类型实现
Debug,集合就能以[elem1, elem2, ...]格式打印——这与仓库 print_debug 章节 中"所有std类型都可用{:?}打印"的说明一致; - 运算结果需收集:四种运算返回迭代器(产出
&i32引用),用.collect::<Vec<&i32>>()收集成Vec后再println!打印;并集、交集与对称差的元素顺序都是任意的,只有差集[1]是确定结果。
在 RBE 项目中的学习路径与延伸阅读
在 SUMMARY.md 中,与哈希容器直接相关的内容编排如下,可作为完整学习路径:
- 标准库类型总览:了解
Box、Vec、String、Option、Result等标准容器的定位; - HashMap(本文主体)→ 自定义键类型 → HashSet:从键值存储到去重集合的递进;
- 向量 Vec:对比"按索引取值"与"按键取值"两种容器模型的差异;
- derive 派生:理解
Eq、Hash、Debug等 trait 自动实现的机制; - Debug 格式化:掌握
{:?}与{:#?}的打印方式,便于调试HashMap/HashSet。
值得一提的是,本仓库所有rust,editable代码块都可在在线编辑器中直接运行(见 book.toml 中[output.html.playpen]的editable = true配置),建议在阅读本文时亲手运行并修改示例,观察insert返回值、get的Option结果以及四种集合运算输出的变化,这是巩固哈希容器知识最快的方式。
小结
本文以 HashMap 章节 为骨架,系统梳理了 Rust 哈希容器的核心要点:
HashMap以键取值,键必须实现Eq与Hash;- 通过
new/with_capacity创建,通过insert/get/remove/iter完成增删查遍历; - 浮点数因精度问题不适合作键,
Vec等容器在元素可哈希时可作复合键; - 自定义类型只需
#[derive(PartialEq, Eq, Hash)]一行即可作为键; HashSet<T>是HashMap<T, ()>的包装,天然去重,并提供union、difference、intersection、symmetric_difference四种集合运算。
掌握这些能力,你就能够在 Rust 项目中安全、高效地组织键值数据与去重集合,并清楚知道何时用HashMap、何时用HashSet、何时改用有序的BTree系列容器。
- 文档
- 教程
【免费下载链接】rust-by-example
Learn Rust with examples (Live code editor included)
相关推荐
Rust 函数精讲:从 `fn` 声明语法到 FizzBuzz 实战(Rust by Example 指南)
Rust 函数精讲:从 fn 声明语法到 FizzBuzz 实战(Rust by Example 指南) 导读 本文以 Rust by Example(RBE)
文档教程Rust 自定义类型精讲:struct、enum 与 const/static 完整指南(基于 Rust by Example)
Rust 自定义类型精讲:struct、enum 与 const/static 完整指南(基于 Rust by Example) Rust 的"自定义类型"(C
文档教程Rust 类型转换实战:深入掌握 `as` 关键字(Rust By Practice 精讲)
Rust 类型转换实战:深入掌握 as 关键字(Rust By Practice 精讲) Rust 是一门拒绝隐式类型转换(coercion)的语言,基本类型之
文档教程示例工程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考