Rust 入门 KV存储HashMap (十七)
和动态数组一样, HashMap 也是Rust 标准库中提供的集合类型,但是又与动态数组不同, HashMap 中存储的是一一映射的KV键值,并提供来平均复杂度为o(1) 的查询方法, 当我们希望通过一个 key 去查询值是,该类型非常有用, 以致于Go 语言将该类型设置成了语言级别的内置特性。
Rust中哈希类型(哈希映射) 为HashMap<K,V> ,在其它语言中,也有类似的数据解构,例如 hashmap ,map,object ,hash table ,字典 等等, 引用小品演员孙涛的一句台词:大家都是本地狐狸,别搁那装貂。
创建HashMap
跟创建动态数组 Vec 的方法类似, 可以使用new 方法来创建 HashMap, 然后通过insert 方法插入键值对。
使用new 方法创建
use std::collections::HashMap;// 创建一个hashmap , 用于存储宝石种类和对应的数量
let mut my_gems = HashMap::new();// 将宝石类型和对应的数量写入表中
my_gems.insert("红宝石",1);
my_gems.insert("蓝宝石",2);
my_gems.insert("河边捡的破石头", 18);
很简单对吧? 跟其它语言没有区别,聪明的同学甚至能够猜到该HashMap的类型: HashMap<&str,i32>.
但是还有一点,你可能没有注意, 那就是使用HashMap 需要手动通过 use... 从标准库中引入到我们当前的作用域中来,仔细回一下,之前使用另外两个结合类型 String 和 Vec 时, 我们是否有手动引用过? 答案是No , 因为HashMap 并没有包含在Rust 的 prelude 中, (Rust 为了简化用户使用,提前将最常用的类型自动引入到作用域中)。
所有的集合类型都是动态的,意味着他们没有固定的内存大小,因此他们底层的数据都存储在内存堆上,然后通过一个存储在栈中的引用类型来访问。 同时,跟其它集合类型一致, HashMap 也是内聚性的, 即所有的K 必须拥有同样的类型, V 也是如此。
跟 Vec 一样, 如果预先知道要存储的KV 对个数, 可以使用 HashMap::with_capacity(capacity) 创建指定大小的 HashMap , 避免频繁的内存分配和拷贝,提升性能。
使用迭代器和 collect 方法创建
在实际使用中, 不是所有的场景都能new 一个哈希表后,然后悠哉悠哉的依次插入对应的键值对,而是可能会从另外一个数据解构中, 获取到对应的数据, 最终生成HashMap 。
例如考虑一个场景,有一张表格中记录来足球联赛中各队伍名称和积分的信息,这张表如果被导入到Rust 项目,一个合理的数据解构是 Vec<String,u32> 类型,该数组中的元素是一个个元组, 该数据解构跟表格数据非常契合: 表格中的数据都是逐行存储,每一个行都存有一个 (队伍名称,积分) 的信息。
但是在很多时候,由需要通过队伍名称来查询对应的积分,此时动态数组就不适用来, 因此可以用HashMap 来保存相关的队伍名称 -> 积分映射关系, 理想很丰满, 现实很骨感,如何将Vec<String,u32> 中的数据快速写入到HashMap<String,u32> 中?
一个动动脚趾头就能想到的笨方法如下:
fn main() {use std::collections::HashMap;let teams_list = vec![ ("中国队".to_string(), 188),("美国队".to_string(),100),("巴西队".to_string(),99),];let mut teams_map = HashMap::new();for team in &teams_list {teams_map.insert(&team.0,team.1);} println!("{:?}",teams_map)
}
遍历列表,将每一个元组作为一对KV 插入到HashMap中, 很简单,但是。。。也不太聪明的颜值,换个词说就是--- 不够rusty.
好在,Rust为我们提供了一个非常精妙的解决办法:先将 Vec转为迭代器,接着通过collect 方法,将迭代器中的元素收集后,转成 HashMap:
fn main() {use std::collections::HashMap;let teams_list = vec![ ("1队".to_string() , 100),("2队".to_string(),18),("3队".to_string(),50),];let teams_map : HashMap<_,_> =teams_list.into_iter().collect();println!("{:?}",teams_map)
}
代码很简单, into_iter() 方法将列表转为迭代器, 接着通过collect 进行收集, 不过需要注意的是, collect方法在内部实际上支持生成多种类型的目标集合,因此我们需要通过类型标注HashMap<_,_> 来告诉编译器 请帮我们收集为 HashMap 集合类型,具体的KV 类型, 麻烦编译器您老人家帮我们推导。
由此可见,Rust中的编译器时而小聪明,时而大聪明, 不过好在,它大聪明的时候,会自家人知道自家事,总归会通知你一声。
error[E0282]: type annotations needed // 需要类型标注
--> src/main.rs:10:9
|
10 | let teams_map = teams_list.into_iter().collect();
| ^^^^^^^^^ consider giving `teams_map` a type // 给予 `teams_map` 一个具体的类型
所有权转移
HashMap 的所有权规则与其它Rust类型没有区别:
1.若类型实现Copy特征, 该类型会被复制进HashMap,因此无所谓所有权
2.若没实现Copy特征,所有权将被转移给HashMap中
例如我参选 帅气男孩是的场景在现:
fn main() {use std::collections::HashMap;let name = String::from("Sunface");let age = 10;let mut handsome_boys = HashMap::new();handsome_bodys.insert(name, age);println!("因为过于无耻,{} 已经被刷下来了",name);println!("还有,他是年龄不止{}岁",age);
}
运行代码如下,报错如下:
error[E0382]: borrow of moved value: `name`
--> src/main.rs:10:32
|
4 | let name = String::from("Sunface");
| ---- move occurs because `name` has type `String`, which does not implement the `Copy` trait
...
8 | handsome_boys.insert(name, age);
| ---- value moved here
9 |
10 | println!("因为过于无耻,{}已经被除名", name);
| ^^^^ value borrowed here after move
提示很清晰,name 是String类型, 因此它受到所有权的限制,在insert时, 它的所有权被转移给handsome_bodys, 所以最后在使用时, 会遇到这个无情但是意料之中的报错。
如果你使用引用类型放入HashMap中, 请确保该引用的生命周期至少跟HashMap 活的一样久;
fn main() {use std::collections::HashMap;let name = String::from("Sunface");let age = 18;let mut handsome_boys = HashMap::new();handsome_boys.insert(&name,age);std::mem::drop(name);println!("因年龄过大,{:?} 已被刷下来",handsome_boys);println!("还有, 他的真实年龄远远不止{} 岁",age);
}
上面代码,我们借用name 获取来它的引用,然后插入到handsome_boys中, 至此一切都很完美,但是紧接着,就通过 drop 函数手动将name 字符串从内存中移除,再然后就报错了:
handsome_boys.insert(&name, age);
| ----- borrow of `name` occurs here // name借用发生在此处
9 |
10 | std::mem::drop(name);
| ^^^^ move out of `name` occurs here // name的所有权被转移走
11 | println!("因为过于无耻,{:?}已经被除名", handsome_boys);
| ------------- borrow later used here // 所有权转移后,还试图使用name
最终,某人因为过于无耻,真正的被除名了
查询HashMap
通过get 方法可以获取元素:
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);
上面有几点需要注意:
1.get 方法返回一个Option<&i32> 类型: 当查询不到时,会返回一个None,查询到时返回Some(&i32)
2. &i32是队HashMap中值的借用,如果不使用借用, 可能会发生所有权的转移
3.get 方法的 key 参数必须是一个引用, 如这里的scores.get(&team_name) , 这是因为HashMap<K,V>的get 方法的签名如下
impl<K,V> HashMap<K,V>
where K: Eq + Hash,
{pub fn get<Q>(&self,k: &Q) -> Option<&V> where K: Borrow<Q>,Q: Hash + Eq + ?Sized,{ ... }
}
4. 可以看到签名中的 k: &Q, 下面的特征约束 K: Borrow<Q> 是指类型 K 需要能以另一种形式Q 被借用。 在这种情况下,String 实现来Borrow<str> , 所以 &String 和 &str 类型都可以用于get方法。
还可以继续拓展下, 上面的代码中,如果我们想直接获得值类型的score 该怎么办。 答案简约但不简单:
let score : i32 = scores.get(&team_name).copied().unwrap_or(0);
这里留给大家一个小作业,去官方文档查询下Option 的copied 方法和 unwrap_or 方法的含义及该如何使用
还可以通过循环的方式依次遍历KV 对:
use std::collections::HashMap;let mut scores = HashMap::new();scoers.insert(String::from("Blue"), 10);
scoers.insert(String::from("Yellow"),55);for (key,value) in &scores {println!("{}: {}",key,value);
}
更新HashMap中的值
更新值的时候,涉及多种情况,咱们在代码中一一进行说明
fn main() {use std::collections::HashMap;let mut scores = HashMap::new();scores.insert("Blue", 10);// 覆盖已有的值let old = scores.insert("Blue",20);assert_eq!(old, Some(10));// 检查新插入的值let new = scores.get("Blue");assert_eq!(new, Some(&20));// 查询 Yellow 对应的值,若不存在则 插入新值 let v = scores.entry("Yellow").or_insert(5);assert_eq!(*v,5); // 不存在 插入5 // 查询yellow 对应的值, 若不存在则插入新值let v =scores.entry("Yellow").or_insert(50);assert_eq!(*v, 5); // 已存在, 因此50 没有插入
}
在已有值 的基础上更新
另一个常用场景如下, 查询某个key 对应的值,若不存在则插入新值,若存在则对已有的值进行更新,例如在文本统计词语出现的次数:
use std::collections::HashMap;let text = "hello world worderful world";let mut map = HashMap::new();// 根据空格来切分字符串( 英文单词都是通过空格切分)
for word in text.split_whitespace() {let count = map.entry(word).or_insert(0);*count += 1;
}println!("{:?}",map);
上面代码中,新建一个map 用于保存词语出现的次数。 插入一个词语时会进行判断: 若之前没有插入过,则使用该词语作key, 插入次数0 作为value , 若之前插入过则取出之前统计的该词语出现的次数,对其 加一。
有亮点值得注意
1.or_insert 返回来&mut v 引用,因此可以通过该可变引用直接修改map 中对应的值
2.使用count 引用时,需要先进行解引用 * count ,否则会出现类型不匹配
哈希函数
你肯定比较好奇, 为何较哈希啊表,到底什么是哈希
先来设想下,如果要实现 Key 与 Value的 一一对应, 是不是意味着我们要能比较两个key的相等性? 例如 'a' 和 'b' , 1 和 2 , 当这些类型的key 且能比较时, 可以很容易知道 1,对应的值不会错误的映射到 2 上, 因为 1 不等于 2 , 因此, 一个类型能否作key 的关键就是是否能进行相等比较,或者说该类型是否实现 了 std::cmp::Eq特征
f32 和 f64浮点数,没有实现std::cmp::Eq特征, 因此不可以用作HashMap 的key 。
好来,理解完这个, 在来设想一点,若一个复杂电的类型作为key ,那么在底层对它进行存储,怎么使用它进行查询和比较 ? 是不是很棘手? 好在我们有哈希函数: 通过它把key 计算后映射为哈希值,然后使用该哈希值来进行储存,查询,比较等操作。
但是问题又来了, 如何保证不同 key 通过哈希后的两个值不会相同?如果相同,那意味着我们使用不同的key , 却查到来同一个结果,这种明显是错误的行为。 此时,就涉及到安全性跟性能 的取舍来。
若要追求安全,尽可能该减少冲突,同时防止拒绝服务(Denial of Service, Dos)攻击, 就要使用密码学安全的哈希函数,HashMap 就是使用来这样的哈希函数,防止若要追求性能。 就需要使用没有那么安全的算法。
高性能三方库
因此若性能测试显示当前标准库默认的哈希函数不能满足你的性能需求,就需要去crates.io 上寻找其它的哈希函数实现,使用方法很简单:
use std::hash::BuildHasherDefault;
use std::collections::HashMap;
// 引入三方哈希函数
use twox_hash::XxHash64;// 指定HashMap使用第三方的哈希函数 XxHash64
let mut hash: HashMap<_,_ ,BuildHasherDefault<XxHash64>> = Default::default();
hash.insert(42,"the answer");
assert_eq!(hash.get(&42),Some(&"the answer"));
目前,HashMap 使用的哈希函数是 SipHash ,它的性能不是很高, 但是安全性很高, SipHash在中等大小的key上,性能相当不错,但是对于小型的 Key (例如整数) 或者大型Key (例如字符串)来说,性能还是不够好,若你需要极致性能,例如实现算法,可以考虑这个库: ahash.
最后,如果你想要了解HashMap 更多的用法,可以从crates.io进行搜索,查看高效方法