用过Python的相信对字典这个概念不陌生,其key-value的存储模式大大方便的开发者进行数据的存储和查找,所以字典在Python的开发中用的非常多,那新兴语言Rust会跟进这一优秀的数据存储结构的设计吗。在此本文的女主角登场了,Rust中的HashMap结构就是类似于Python的字典角色。
HashMap
类型储存了一个键类型 K
对应一个值类型 V
的映射。它通过一个 哈希函数(hashing function)来实现映射,决定如何将键和值放入内存中。它通过一个 哈希函数(hashing function)来实现映射,决定如何将键和值放入内存中。需要注意的是HashMap
存储的数据是放在堆内存里的。
HashMap可以用于需要任何类型作为键来寻找数据的情况,而不是像 vector 那样通过索引。类似于数据库的设计,只要我们在存储数据的时候设计好value的key,那么我们就可以根据key立马得到我们想要的value。
一:HashMap创建
use std::collections::HashMap;
fn main() {
let mut person = HashMap::new();
}
我们用HashMap的new函数来创建一个新的空的HashMap,需要注意的是我们开头用了use std::collections::HashMap;来引进HashMap。那是因为并没有被 prelude 自动引用。标准库中对 HashMap
的支持也相对较少。在这我个人感觉HashMap这种好用的结构没有得到应有的尊重。
另外在学习Rust的小伙伴也知道,Rust语言为了保证安全性,通常会加很多严格的校验,HashMap也一样。像 vector 一样,哈希 map 将它们的数据储存在堆上,类似于 vector,哈希 map 是同质的:所有的键必须是相同类型,值也必须都是相同类型。相比于Python字典的包容性,感觉Rust在设计字典的时候就和小姑娘谈恋爱约会一样,怕你不来,又怕你乱来。设计者怕你不用HashMap,又怕你乱用HashMap。
HashMap插入常用的有两种方法,一是通过insert方法,二是使用一个元组的 vector 的 collect
方法。
先来看看insert方法,我们用名字作为索引,value对应名字的年龄,构成name-age的键值对。
use std::collections::HashMap;
fn main() {
let mut person = HashMap::new();
person.insert(String::from("ftz"), 30);
person.insert(String::from("zky"), 18);
println!("{:?}",person);
}
运行结果:
第二种构建哈希 map 的方法是使用一个元组的 vector 的 collect
方法,其中每个元组包含一个键值对。collect
方法可以将数据收集进一系列的集合类型,包括 HashMap
。例如,如果所有人的名字和年龄分别在两个 vector 中,可以使用 zip
方法来创建一个元组的 vector,可以把zip想象成一个拉链,连接两个元组,其中 “ftz” 与 30 是一对,依此类推。接着就可以使用 collect
方法将这个元组 vector 转换成一个 HashMap。
use std::collections::HashMap;
fn main() {
let name = vec!["ftz","zky"];
let age = vec![30, 18];
let person: HashMap<_,_> = name.iter().zip(age.iter()).collect();
println!("{:?}",person);
}
运行结果:
需要注意的是 HashMap<_, _>
类型标注是必要的,因为 collect
有可能当成多种不同的数据结构,而除非显式指定否则 Rust 无从得知你需要的类型。但是对于键和值的类型参数来说,可以使用下划线占位,而 Rust 能够根据 vector 中数据的类型推断出 HashMap
所包含的类型。
有插入就会有更新,上面的例子,人名对应的年龄,人名是不变的,但是年龄会一年增长一岁,就需要对value进行更新。
对HashMap的更新存在两种情况,如何处理一个键已经有值了的情况。可以选择完全无视旧值并用新值代替旧值。可以选择保留旧值而忽略新值,并只在键 没有 对应值时增加新值。或者可以结合新旧两值。
use std::collections::HashMap;
fn main() {
let mut person = HashMap::new();
person.insert(String::from("ftz"), 30);
person.insert(String::from("zky"), 18);
person.insert(String::from("zky"), 19);
println!("{:?}",person);
}
运行结果:
这里需要解释一下什么是选择更新覆盖,就是覆盖是有前提的,那就是要插入的key-value键值对在现有的HashMap中key不存在的情况下,可以直接插入,但是在key存在的情况下就不进行插入。
use std::collections::HashMap;
fn main() {
let mut person = HashMap::new();
person.insert(String::from("ftz"), 30);
person.insert(String::from("zky"), 18);
person.entry(String::from("ftz")).or_insert(31);
person.entry(String::from("ftzSon")).or_insert(1);
println!("{:?}",person);
}
运行结果:
哈希 map 有一个特有的 API,叫做 entry
,它获取我们想要检查的键作为参数。entry
函数的返回值是一个枚举,Entry
,它代表了可能存在也可能不存在的值。Entry
的 or_insert
方法在键对应的值存在时就返回这个值的可变引用,如果不存在则将参数作为新值插入并返回新值的可变引用。
另一个常见的哈希 map 的应用场景是找到一个键对应的值并根据旧的值更新它。例如,下面代码计数一些文本中每一个单词分别出现了多少次。我们使用哈希 map 以单词作为键并递增其值来记录我们遇到过几次这个单词。如果是第一次看到某个单词,就插入值 0
use std::collections::HashMap;
fn main() {
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);
}
运行结果:
or_insert
方法事实上会返回这个键的值的一个可变引用(&mut V
)。这里我们将这个可变引用储存在 count
变量中,所以为了赋值必须首先使用星号(*
)解引用 count
。这个可变引用在 for
循环的结尾离开作用域,这样所有这些改变都是安全的并符合借用规则。
拿上面的一个例子,名字对应的年龄过了一年都会长一岁
use std::collections::HashMap;
fn main() {
let mut person = HashMap::new();
person.insert(String::from("ftz"), 30);
person.insert(String::from("zky"), 18);
let mut person2 = HashMap::new();
for (key, value) in person {
let age = person2.entry(key.to_string()).or_insert(value);
*age += 1;
}
println!("{:?}",person2);
}
运行结果:
可以通过 get
方法并提供对应的键来从哈希 map 中获取值
fn main() {
let mut person = HashMap::new();
person.insert(String::from("ftz"), 30);
person.insert(String::from("zky"), 18);
let ftz_age = person.get("ftz");
println!("ftz age is {:?}",ftz_age);
}
运行结果:
可以看到打印出来的是Some(30),说明 get
返回 Option
,所以结果被装进 Some
;如果某个键在哈希 map 中没有对应的值,get
会返回 None
。这时候正规的写法就是用match
use std::collections::HashMap;
fn main() {
let mut person = HashMap::new();
person.insert(String::from("ftz"), 30);
person.insert(String::from("zky"), 18);
let ftz_age = person.get("ftz");
match ftz_age{
Some(ftz_age) => println!{"ftz age is {}",ftz_age},
None => println!{"no ftz"},
}
}
运行结果:
我们用for循环来遍历HashMap
use std::collections::HashMap;
fn main() {
let mut person = HashMap::new();
person.insert(String::from("ftz"), 30);
person.insert(String::from("zky"), 18);
for (key,value) in &person {
println!("the {} age is {}",key,value);
}
}
运行结果:
对于像 i32
这样的实现了 Copy
trait 的类型,其值可以拷贝进哈希 map。对于像 String
这样拥有所有权的值,其值将被移动而哈希 map 会成为这些值的所有者。
use std::collections::HashMap;
fn main() {
let mut person = HashMap::new();
let ftz_name = String::from("ftz");
let ftz_age = 30;
person.insert(ftz_name, ftz_age);
println!("the {} age is {}",ftz_name,ftz_age);
}
可以看到上面代码给出的提示ftz_name的所有权被移动了,即在打印的时候已经不能访问ftz_name这个变量了,但是由于ftz_age的类型是i32其值是拷贝进去的,所有仍然有其所有权。如果将值的引用插入到HashMap,值本身不会移动也就能正常访问了
use std::collections::HashMap;
fn main() {
let mut person = HashMap::new();
let ftz_name = String::from("ftz");
let ftz_age = 30;
person.insert(&ftz_name, ftz_age);
println!("the {} age is {}",ftz_name,ftz_age);
}
运行结果:
Rust中的HashMap实现了Python中的字典结构,但是条条框框太多了,想用好确实不容易,至少我学完还是没有完全能掌握,后面继续学习才能深入理解Rust。