Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

8.5 HashMap Pt.1:HashMap的定义、创建、合并与访问

8.5.0. 本章内容

第八章主要讲的是 Rust 中常见的集合。Rust 提供了很多集合类型的数据结构,这些集合可以包含很多值。但是第八章所讲的集合与数组和元组有所不同。

第八章中的集合是存储在堆内存上而非栈内存上的,这也意味着这些集合的数据大小无需在编译时就确定,在运行时它们可以动态地变大或变小。

本章主要会讲三种集合:Vector、String 和 HashMap(本文)

8.5.1. 什么是 HashMap

HashMap 的形式是 HashMap<K, V>,其中 K 代表键(key),V 代表值(value)。HashMap 以键值对的形式存储数据,一个键对应一个值。很多语言都支持这样的集合数据结构,但叫法不一定相同——例如 C# 中相同概念的数据结构叫字典(dictionary)。

HashMap 的内部实现使用了哈希函数,这个函数决定了如何在内存中存储键与值。

Vector 中我们使用索引来访问数据,但有的时候你想通过键来查找数据(键可以是任何实现了 EqHash trait 的类型),而不是通过索引,或者说你不清楚这个数据在哪个索引上。这种情况就可以使用 HashMap

需要注意的是,HashMap同构的,也就是说在一个 HashMap 中,所有的键必须是同一类型,所有的值必须是同一类型。

8.5.2. 创建 HashMap

  • 由于 HashMap 不常用,所以 Rust 并没有把它放进预导入模块(prelude)。使用前需要引入 HashMap,在文件开头写上:use std::collections::HashMap;
  • 创建空的 HashMap 使用 HashMap::new() 函数
  • 添加数据使用 insert() 方法

看个例子:

use std::collections::HashMap;

fn main() {
    let mut scores: HashMap<String, i32> = HashMap::new();
}

在这里创建了一个名为 scores 的变量来存储 HashMap。由于 Rust 是强类型语言,它必须知道你在 HashMap 里存储什么数据类型。又因为没有前后文可供编译器推断,所以在声明时就必须把键和值的数据类型显式声明出来。在这段代码中,scores 的键被设为 String,值被设为 i32

当然,如果你在后文给这个 HashMap 添加了数据,Rust 就会根据插入的数据自动推断键和值的数据类型。添加数据使用 insert() 方法。例子如下:

use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();
    scores.insert(String::from("dev1ce"), 0);
}

因为在第 5 行往 scores 里插入了键值对,且键 String::from("dev1ce")String 类型,值 0i32(Rust 默认整数类型是 i32),所以编译器就会推断出 scores 是一个 HashMap<String, i32>,因此第四行在声明时就不需要显式声明类型了。

8.5.3. 将两个 Vector 合为一个 HashMap

在元素类型为元组的 Vector 上,可以使用 collect 方法来构建 HashMap。换个说法:假如你有两个 Vector,这两个 Vector 上的所有值都有一一对应关系,就可以使用 collect,把一个 Vector 里的数据作为键,另一个作为值,放到 HashMap 里。如下例:

use std::collections::HashMap;

fn main() {
    let player = vec![String::from("dev1ce"), String::from("Zywoo")];
    let initial_scores = vec![0, 100];
    let scores: HashMap<_, _> = player.into_iter().zip(initial_scores).collect();
}
  • player 这个 Vector 用来存储选手名字,里面的元素是 String 类型。
  • initial_scores 这个 Vector 用来存储每个选手对应的得分。
  • player.into_iter() 以及传给 zipinitial_scores(也会变成迭代器)会从这两个 Vector 中取出拥有所有权的值。使用 .zip() 可以创建“player 中元素在前、initial_scores 中元素在后”的元组序列。如果想交换元素位置,把代码中的两个迭代器互换位置即可。然后再使用 .collect() 把这些元组转换成 HashMap。因为使用了 into_iterHashMap 会获得这些值的所有权(原来的两个 Vector 会被消耗掉)。
  • 最后要注意的一点是:.collect() 支持转换为很多数据结构。如果写代码时不显式声明其类型,程序就会报错。这里指明了类型是 HashMap<_, _><> 中的两个数据类型,编译器可以根据代码(也就是两个 Vector 的类型)来推断,所以这里可以写 _ 占位符让它自行推断。

8.5.4. HashMap 和所有权

对于实现了 Copy trait 的数据类型(例如 i32 以及绝大多数简单数据类型),值会被复制到 HashMap 中,原先的变量仍然可用。对于没有实现 Copy 的类型(例如 String),所有权会被移交给 HashMap

如果将引用插入到 HashMap,值本身就不会移动。在 HashMap 的有效期间,被引用的值必须保持有效。

8.5.5. 访问 HashMap 中的值

访问值可以使用 get 方法。get 方法的参数是 HashMap 的键,返回值是 Option<&V> 枚举。看个例子:

use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();
    scores.insert(String::from("dev1ce"), 0);
    scores.insert(String::from("Zywoo"), 100);

    let player_name = String::from("dev1ce");
    let score = scores.get(&player_name);
    match score {
        Some(score) => println!("{}", score),
        None => println!("Player not found"),
    };
}
  • 首先创建了一个空的 HashMap 叫做 scores,然后通过 insert 往里面添加了两个键值对 ("dev1ce", 0)("Zywoo", 100),键类型是 String,值类型是 i32
  • 然后声明了名为 player_nameString 变量,其值为 "dev1ce"
  • 接着通过 HashMap 上的 get 方法,在 scores 中查找 player_name 这个键所对应的值(& 表示引用)。但由于 get 返回的是 Option 枚举,所以这里先把这个 Option 值赋给 score,后面再来解包。
  • 最后使用 match 表达式来处理 score。如果找到了对应的值,score 就是 Some 变体,把 Some 关联的值绑定到 score 上,然后再打印出来。如果找不到,score 就是 None 变体,这时就会打印 "Player not found"

输出:

0

8.5.6. 遍历 HashMap

遍历 HashMap 一般使用 for 循环。如下例:

use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();
    scores.insert(String::from("dev1ce"), 0);
    scores.insert(String::from("Zywoo"), 100);
    for (k, v) in &scores {
        println!("{}: {}", k, v);
    }
}

这个 for 循环使用的是 HashMap 的引用,也就是 &scores,因为通常遍历之后还要继续使用这个 HashMap,所以使用引用就不会失去所有权。前面的 (k, v) 是模式匹配:第一个值是键,赋给 k;第二个是值,赋给 v

一种可能的输出如下(遍历顺序是任意的,不同运行之间可能不同):

dev1ce: 0
Zywoo: 100