Rust By Example 精讲:HashMap 与 HashSet 键值容器实战指南

发布时间:2026/10/6 1:48:14
Rust By Example 精讲:HashMap 与 HashSet 键值容器实战指南
文档教程【免费下载链接】rust-by-exampleLearn Rust with examples (Live code editor included)项目地址https://gitcode.com/gh_mirrors/ru/rust-by-example点击查看免费下载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。键的约束EqHashHashMap之所以要求键实现Eq和Hash是因为哈希表的工作方式Hash把键映射为一个哈希值用于快速定位桶bucket的位置Eq在哈希值相同的碰撞场景下用相等性判断确认两个键是否真的是同一个键。本仓库原文明确指出HashMap的键可以是布尔值、整数、字符串或任何实现了Eq和Hashtrait 的类型并提示更多内容见下一节即 自定义键类型。创建 HashMapnew与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 Were sorry, the call cannot be completed as dialed. Please hang up and try again., 645-7689 Hello, this is Mr. Awesomes 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 OptionV match contacts.get(Daniel) { Some(number) println!(Calling Daniel: {}, call(number)), _ println!(Dont have Daniels 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!(Dont have Ashleys 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只取引用返回OptionVcontacts.get(Daniel)接收的是键的引用返回OptionV键存在时返回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)传键引用返回OptionVremove(k)传键引用移除并返回OptionV被移除的值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。例如VecT在T实现Hash时也会实现Hash因此VecT这类复合结构也能成为键。一行代码让自定义类型成为键对自定义类型而言只需一行派生#[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 Accounta{ username: a str, password: a str, } struct AccountInfoa{ name: a str, email: a str, } type Accountsa HashMapAccounta, AccountInfoa; fn try_logona(accounts: Accountsa, 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.everymanemail.com, }; accounts.insert(account, account_info); try_logon(accounts, j.everyman, psasword123); try_logon(accounts, j.everyman, password123); }这个例子的核心价值在于组合键Account { username, password }作为一个整体参与哈希与相等比较登录时构造同构的临时Account去查询类型别名type Accountsa HashMapAccounta, AccountInfoa让复杂签名变得可读两次调用对比第一次密码拼错psasword123查询失败第二次密码正确password123查询成功直观展示get基于Eq精确匹配的语义。HashSet只关心键的去重集合如果说HashMap存储键值对那么HashSet就是只关心键、不关心值的集合。正如 HashSet 章节 所说HashSetT实质上只是HashMapT, ()的包装——值类型被占位为单元类型()。这带来的核心契约是HashSet保证集合内没有重复元素这是任何集合set类型都要满足的约定。当插入一个已存在的值新旧值相等且哈希相同时新值会替换旧值。为什么不用Vec存键——因为去重是HashSet的天然能力你无需自己写contains检查它特别适合每样东西只保留一份或判断是否已拥有某物的场景。仓库原文同时提示Rust 还提供有序替代实现BTreeSet。四大集合运算HashSet有四种主要操作全部返回迭代器需配合.collect()收集为Vec等容器操作语义union两个集合中所有不重复元素并集difference只在第一个集合、不在第二个集合的元素差集intersection同时出现在两个集合中的元素交集symmetric_difference出现在其中一个集合、但不同时出现在两个集合的元素对称差完整示例集合运算演练下面的示例改编自标准库文档完整演示上述四种运算摘自 hashset.mduse std::collections::HashSet; fn main() { let mut a: HashSeti32 vec![1i32, 2, 3].into_iter().collect(); let mut b: HashSeti32 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 collections 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::Veci32()); // This should print [1] println!(Difference: {:?}, a.difference(b).collect::Veci32()); // Print [2, 3, 4] in arbitrary order. println!(Intersection: {:?}, a.intersection(b).collect::Veci32()); // Print [1, 5] println!(Symmetric Difference: {:?}, a.symmetric_difference(b).collect::Veci32()); }这个例子值得注意的细节从迭代器收集vec![1i32, 2, 3].into_iter().collect()演示了Vec到HashSet的经典转换collect依赖目标类型推断因此a、b必须显式标注HashSeti32insert 的返回值a.insert(4)因 4 是新元素返回trueb.insert(4)因 4 已存在返回false示例中用assert!演示了这一行为注释掉该行即可修复Debug 输出只要元素类型实现Debug集合就能以[elem1, elem2, ...]格式打印——这与仓库 print_debug 章节 中所有std类型都可用{:?}打印的说明一致运算结果需收集四种运算返回迭代器产出i32引用用.collect::Veci32()收集成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)]一行即可作为键HashSetT是HashMapT, ()的包装天然去重并提供union、difference、intersection、symmetric_difference四种集合运算。掌握这些能力你就能够在 Rust 项目中安全、高效地组织键值数据与去重集合并清楚知道何时用HashMap、何时用HashSet、何时改用有序的BTree系列容器。赞分享文档教程【免费下载链接】rust-by-exampleLearn Rust with examples (Live code editor included)项目地址https://gitcode.com/gh_mirrors/ru/rust-by-example点击查看免费下载相关推荐Rust 函数精讲从 fn 声明语法到 FizzBuzz 实战Rust by Example 指南Rust 函数精讲从 fn 声明语法到 FizzBuzz 实战Rust by Example 指南 导读 本文以 Rust by ExampleRBE文档教程Rust 自定义类型精讲struct、enum 与 const/static 完整指南基于 Rust by ExampleRust 自定义类型精讲struct、enum 与 const/static 完整指南基于 Rust by Example Rust 的自定义类型C文档教程Rust 类型转换实战深入掌握 as 关键字Rust By Practice 精讲Rust 类型转换实战深入掌握 as 关键字Rust By Practice 精讲 Rust 是一门拒绝隐式类型转换coercion的语言基本类型之文档教程示例工程上一篇doocs/leetcode 题解精讲面试题 02.05 链表求和逆序存储数位的模拟加法实现下一篇Pegdown完全指南Java开发者必备的Markdown解析神器创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考