5分钟速读之Rust权威指南(十四)

353 阅读4分钟

通用集合类型-哈希映射HashMap

HashMap可以理解为JS中的Map类,可以存储一些键值对的映射关系,哈希映射在内部实现中使用了哈希函数,这同时决定了它在内存中存储键值对的方式。

创建HashMap

哈希映射也是同质的,它要求所有的键必须拥有相同的类型,所有的值也必须拥有相同的类型:

// 从标准库导入
use std::collections::HashMap;

// 使用new方法创建哈希映射
let mut scores = HashMap::new();

// 插入字符串类型的键和数字类型的值
scores.insert(String::from("xiaoming"), 10);
scores.insert(String::from("xiaozhang"), 20);

println!("{:?}", scores);
// {"xiaoming": 10, "xiaozhang": 20}

// 尝试插入其他类型
scores.insert(10, 20); // 报错, 键的类型不同

还可以通过Vec来创建:

// 创建一个Vec<String>
let teams = vec![
  String::from("xiaoming"),
  String::from("xiaozhang")
];

// 初始化Vec<String>中每个人对应的分数
let initial_scores = [10, 20];

// 使用zip方法将teams和initial_scores进行结合
// 使用collect将结果重新生成HashMap<_, _>类型
let scores: HashMap<_, _> = teams.iter().zip(initial_scores.iter()).collect();
// 注意这里的类型标记HashMap<_, _>不能被省略,
// 因为collect可以作用于许多不同的数据结构,如果不指明类型的话,
// rust就无法知道我们具体想要的类型。但是对于键值的类型参数,
// 我们则使用了下画线占位,
// 因为rust能够根据动态数组中的数据类型来推导出哈希映射所包含的类型。

println!("{:?}", scores);
// {"xiaoming": 10, "xiaozhang": 20}

哈希映射与所有权

对于那些实现了Copy trait的类型,例如i32,它们的值会被简单地复制到哈希映射中:

let mut map = HashMap::new();

// 创建i32类型数据
let num1 = 1;
let num2 = 2;

// 插入哈希映射时会自动复制
map.insert(num1, num2);

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

// 两个数字仍然可用
println!("{:?}", num1); // 1
println!("{:?}", num2); // 2

对于String这种持有所有权的值,其值将会转移且所有权会转移给哈希映射:

let mut map = HashMap::new();

// 创建字符串类型数据
let str1 = String::from("str1");
let str2 = String::from("str2");

// 插入哈希映射时会发生所有权转移
map.insert(str1, str2);

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

println!("{:?}", str1); // error, 没有了所有权
println!("{:?}", str2); // error, 没有了所有权

假如我们只是将值的引用插入哈希映射,那么这些值是不会被移动到哈希映射中的。这些引用所指向的值必须要保证,在哈希映射有效时自己也是有效的。后边后介绍“使用生命周期保证引用的有效性”

获取哈希映射中的值

使用get方法返回一个Option<&V>,注意get方法的参数和Option包含的是值的引用:

let str1 = String::from("str1");
let str2 = String::from("str2");

let mut map = HashMap::new();
map.insert(str1, str2);

// 获取到的是值的引用类型
println!("{:?}", map.get("str1")); // Some("str2")
println!("{:?}", map.get(&String::from("str1"))); // Some("str2")

// 获取不到将返回None
println!("{:?}", map.get("str2")); // None

可以使用for循环对哈希映射进行迭代:

let mut map = HashMap::new();
let num1 = 1;
let num2 = 2;
map.insert(num1, num2);

// 每次迭代会得到一个包含键值对的元组
for (key, val) in &map {
  println!("{}-{}", key, val); // 1-2
}

更新哈希映射

当我们将一个键值对插入哈希映射后,接着使用同样的键并配以不同的值来继续插入,之前的键所关联的值就会被替换掉:

let mut map = HashMap::new();
let num1 = 1;
let num2 = 2;
map.insert(num1, num2);

println!("{:?}", map.get(&num1)); // Some(2)

// 执行覆盖
map.insert(num1, &3);

println!("{:?}", map.get(&num1)); // Some(3)

在实际工作中,我们常常需要检测一个键是否存在对应值,如果不存在,则为它插入一个值。哈希映射中提供了一个被称为entry的专用API来处理这种情形,它接收我们想要检测的键作为参数,并返回一个叫作Entry的枚举作为结果:

let mut map = HashMap::new();
let num1 = 1;
let num2 = 2;

map.insert(num1, num2);
println!("{:?}", map); // {1: 2}

// entry(x).or_insert(y)当不存在x时,则插入y,
// 如果已存在,则什么都不做,最后返回x映射的值
let val = map.entry(1).or_insert(3); // 1已存在,没有进行任何操作
println!("{}", val); // 2
println!("{:?}", map); // {1: 2}

map.entry(2).or_insert(3); // 2不存在,插入3
println!("{}", val); // 3
println!("{:?}", map); // {2: 3, 1: 2}

下面来实现一个小功能,利用哈希映射计算一个单词在句子中出现的次数:

let text = "hello world wonderful world";
let mut map = HashMap::new();

// 根据空格拆分text字符串,然对进行迭代
for word in text.split_whitespace() {
  // 如果word不存在则初始化为0,表示当前单词数量
	let count = map.entry(word).or_insert(0);
  // 单词数量 + 1
	*count += 1
}
println!("{:?}", map);
// {"world": 2, "hello": 1, "wonderful": 1}

注意上面count加1的位置,必须首先使用星号(*)来对count进行解引用,后边会讲到解引用。由于这个可变引用会在for循环的结尾处离开作用域,所以我们在代码中的所有修改都是安全且满足借用规则的。