首页 / Rust 入门教程 / HashMap 哈希映射

Rust 入门教程

HashMap 哈希映射

本教程共 78 篇 · 第 38 篇 · 更新于 2026-08-08 · 约 9 分钟阅读

RustRust 入门教程HashMap键值对entryor_insert哈希集合

本节目标:用 HashMap<K, V> 存键值对,学会创建、查询、遍历,并理解 entry + or_insert 在”有则改、无则插”场景下的妙用。

数组靠下标找值,HashMap 靠”键”找值。当你想用队伍名查积分、用用户名查资料时,它比数组顺手得多,平均查找速度是 O(1)。别的语言里它叫 map、字典、object,换汤不换药。

1-1 创建 HashMap

Vec 类似,用 new 创建再 insert 键值对。但有个不同:用 HashMap 得手动 use 引入,因为它没在 Rust 的 prelude(预导入)里——StringVec 因为太常用,被自动引入了,HashMap 得自己写。所谓 prelude,就是不用写 use 就自动能用的那一小撮名字;HashMap 不在其中,所以你每次都得自己从 std::collections 请它出来。

use std::collections::HashMap;

let mut my_gems = HashMap::new();
my_gems.insert("红宝石", 1);
my_gems.insert("蓝宝石", 2);

编译器能推出类型是 HashMap<&str, i32>。和别的集合一样,HashMap 也活在堆上,且所有键必须同类型、所有值必须同类型。

Tip

预先知道大概要存多少对,用 HashMap::with_capacity(n) 一次性预留空间,少做几次内存搬移,性能更好。

1-2 用迭代器 collect 创建

有时数据本来在别的结构里,比如一张积分表读进来是 Vec<(String, u32)>。笨办法是循环 insert,更 Rust 的写法是用迭代器加 collect

use std::collections::HashMap;

let teams_list = vec![
    ("中国队".to_string(), 100),
    ("美国队".to_string(), 10),
    ("日本队".to_string(), 50),
];

let teams_map: HashMap<_, _> = teams_list.into_iter().collect();
println!("{:?}", teams_map);

into_iter 把列表变成迭代器,collect 把里面的元组 (K, V) 收集成 HashMap。注意类型标注 HashMap<_, _> 不能省——collect 能生成多种集合,编译器得靠这个标注才知道你想要 HashMap,具体的 KV 交给它推导。

1-3 所有权会转移

HashMap 遵守普通的所有权规则:键或值若实现了 Copy(如整数),就复制进去;若没实现(如 String),所有权就被转移到 HashMap 里,原变量不能再用了。

use std::collections::HashMap;

let name = String::from("Sunface");
let age = 18;

let mut boys = HashMap::new();
boys.insert(name, age);

// println!("{name}"); // 报错:name 的所有权已转移
Warning

如果把引用放进 HashMap,要保证人引用的那个值活得比 HashMap 久。一旦原值被释放,里面的引用就成了悬垂引用,编译器会在编译期拦下你。

1-4 查询与遍历

get 按 key 取值,返回 Option<&V>:查不到是 None,查到是 Some(&值)

use std::collections::HashMap;

let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);

let team_name = String::from("Blue");
let score: Option<&i32> = scores.get(&team_name);

get 的参数是引用(&team_name),因为写在签名里的是 k: &Q。得益于 String 实现了 Borrow<str>,你传 &String&str 都行。

想直接拿到值而不是引用,可以:

let score: i32 = scores.get(&team_name).copied().unwrap_or(0);

copied&i32 复制成 i32unwrap_or(0) 在查不到时给个默认值 0。

遍历所有键值对用 for

for (key, value) in &scores {
    println!("{key}: {value}");
}
Note

HashMap 内部不保证顺序,遍历出来的先后和插入顺序未必一致,别依赖它。

1-5 更新值:覆盖与 entry

insert 同一个 key,新值会覆盖旧值,并返回旧值的 Option

let old = scores.insert("Blue", 20);
assert_eq!(old, Some(10)); // 返回被覆盖的旧值

更常见的需求是”有就更新、没有就插入”。entry + or_insert 一句话搞定:

let v = scores.entry("Yellow").or_insert(5);
assert_eq!(*v, 5);              // 没有,插入 5

let v = scores.entry("Yellow").or_insert(50);
assert_eq!(*v, 5);              // 已经有了,50 没插进去

or_insert 返回的是指向值的可变引用 &mut V。拿到它就能直接改,不用先查再插。

1-6 一个综合例子:词频统计

统计一句话里每个词出现几次,是 entry 的经典用法:

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);
    *count += 1; // 解引用后才能改
}

println!("{:?}", map);

每遇到一个词:若没见过,用这个词作 key、插入计数 0;若见过,取出已有的计数加一。or_insert 给的可变引用让我们就地修改,干净利落。

Warning

初学者常漏掉 *count += 1 里的解引用。这里 count&mut i32,直接 count += 1 类型对不上,必须先 *count 解引用再改。

1-7 什么样的类型能当键

想当 key,类型必须能比较相等,也就是实现 EqHash 这两个 trait。常见整数、字符串都没问题。但 f32f64 浮点数没实现 Eq(因为存在 NaN,它和谁都不等),所以不能用浮点数作 HashMap 的 key。

底层原理是:哈希函数把 key 算成一个哈希值,再用这个值来存和查。Rust 默认用 SipHash,安全性高但速度一般。若性能测试显示不够快,可去 crates.io 找 ahash 等更快的哈希实现替换。

1-8 HashMap 的两个坑:顺序与键

HashMap 时有两点必须记牢。第一,遍历顺序是不确定的。Rust 出于安全考虑,哈希表使用了随机化的哈希(SipHash),所以你每次运行程序、甚至同一程序不同进程里,迭代出来的键顺序都可能不一样,也绝对不是插入顺序、更不是排序顺序。任何”依赖顺序”的逻辑都是错的。

第二,键类型必须同时实现 HashEq(可比较相等)。像 i32Stringbool 这些基础类型都满足;你自己定义的结构体要当键,就得手动 derive(Hash, Eq, PartialEq)。此外,键在被放进 HashMap 之后就不该再被修改——一旦改了参与哈希的字段,之后就找不回它了。

use std::collections::HashMap;
let mut m: HashMap<String, i32> = HashMap::new();
m.insert(String::from("a"), 1);
// 遍历顺序不保证是 a 先、b 后
for (k, v) in &m {
    println!("{k}: {v}");
}
Warning

别指望 HashMap 有序。需要有序请用 BTreeMap(按键排序,但键要能比较大小)。另外频繁插入前可用 HashMap::with_capacity(n) 预留容量,减少扩容。

1-9 entry API 再深入:计数与默认值

entryHashMap 最高频的利器,尤其做”计数”和”找不到就给默认值”时。它的 or_insert 返回”这个值的可变引用”,如果键不存在就先插入默认值再返回:

use std::collections::HashMap;
let mut counts: HashMap<String, i32> = HashMap::new();
for word in ["a", "b", "a", "a", "b"] {
    let key = word.to_string();
    *counts.entry(key).or_insert(0) += 1; // 首次出现给 0,然后 +1
}
// counts: a->3, b->2

如果默认值的构造比较贵,用 or_insert_with(|| 贵的计算) 做”惰性”插入——只有真正需要默认值时才计算,避免白白浪费。

Tip

做词频、做分组统计、做缓存,entry().or_insert(...) 几乎都是最干净写法。记住它返回的是 &mut V,所以配合 * 解引用去修改值。

1-10 用 HashMap 做分组与倒排

除了计数,HashMap 还特别适合”按某个键把数据分堆”。比如你有一批学生,想按”班级”分组,就以学生号作值、班级名作键,用 entry(班级).or_insert_with(Vec::new) 拿到该班的列表再 push。这样一次遍历就得到”班级 → 学生列表”的映射。

另一个经典用法是”倒排索引”:正常是”文档 → 包含的词”,有时你需要反过来查”某个词出现在哪些文档”。做法是遍历每个文档的每个词,把词当键、文档编号收进 Vec 当值。这种”一对多”的关系,用 HashMap<K, Vec<V>> 表达最自然。

做这类聚合时要记住两点:键必须可哈希且相等比较稳定;往 Vecpush 不会改变键本身,所以不会影响查找。聚合类的需求(分组、计数、分类、索引),基本都能用 HashMapentry 一把梭搞定。

Tip

看到”按 X 归拢 Y""统计每个 X 有多少 Y""从 Y 反查 X”这类描述,第一时间就想到 HashMap 配合 entry().or_insert(...),几乎不会错。

1-11 更多常用操作

HashMap 还有一些高频方法,先认个脸熟:

use std::collections::HashMap;

let mut m: HashMap<String, i32> = HashMap::new();
m.insert("a".into(), 1);

println!("{}", m.contains_key("a")); // true:有没有这个键
println!("{}", m.len());              // 1:有多少对
println!("{}", m.is_empty());         // false

m.remove("a");                        // 删除某个键及其值
println!("{}", m.contains_key("a"));  // false

除了 entry(key).or_insert(默认),“有就改、没有就插”还有个更精致的写法——and_modify 配合 or_insert

use std::collections::HashMap;

let mut m: HashMap<&str, i32> = HashMap::new();
m.entry("a")
 .and_modify(|v| *v += 1) // 键存在时执行
 .or_insert(1);           // 键不存在时插入 1

第一次执行时 a 不存在,and_modify 里的闭包不触发,最终插入 1;第二次 a 已存在,and_modify 把它加一变成 2。一行把”存在则更新、不存在则初始化”写全了。

另外,当默认值就是类型的”零值”时,可以用 or_default() 偷懒,它会调用该类型的 Default 实现(比如 i32 的默认是 0Vec 的默认是空向量):

let mut m: HashMap<&str, i32> = HashMap::new();
*m.entry("x").or_default() += 5; // 默认 0,加 5 后变成 5
Note

初始化时若数据已经是”键-值”元组的集合,也可用 HashMap::from([(k1, v1), (k2, v2)]) 直接造,比一个个 insert 更紧凑。

1-12 小结

  • HashMap<K, V> 存键值对,需 use std::collections::HashMap,查找约 O(1);
  • 创建用 new + insert,或从迭代器 collect 而来(标注 HashMap<_, _>);
  • 没实现 Copy 的键/值,所有权会转移进 HashMap
  • 查询用 get 返回 Option<&V>,遍历用 for (k, v)
  • 有则改无则插,用 entry(key).or_insert(默认)
  • 能当 key 的类型需实现 Eq + Hash,浮点数不行。

集合类型(Vec、String、HashMap)告一段落。下一章进入错误处理,先看不可恢复的 panic!