Update: unified format

pull/369/head
Allan Downey 3 years ago
parent 53ac231dea
commit 2aab841de0

@ -1,8 +1,8 @@
# KV存储HashMap
和动态数组一样,`HashMap` 也是 Rust 标准库中提供的集合类型,但是又与动态数组不同,`HashMap` 中存储的是一一映射的 `KV `键值对,并提供了平均复杂度为 `O(1)` 的查询方法,当我们希望通过一个 `Key` 去查询值时,该类型非常有用,以致于 `Go语言` 将该类型设置成了语言级别的内置特性。
和动态数组一样,`HashMap` 也是 Rust 标准库中提供的集合类型,但是又与动态数组不同,`HashMap` 中存储的是一一映射的 `KV `键值对,并提供了平均复杂度为 `O(1)` 的查询方法,当我们希望通过一个 `Key` 去查询值时,该类型非常有用,以致于 Go 语言将该类型设置成了语言级别的内置特性。
Rust 中哈希类型(哈希映射)为 `HashMap<K,V>`,在其它语言中,也有类似的数据结构,例如 `hash map``map``object``hash table``字典`等等,引用小品演员孙涛的一句台词:大家都是本地狐狸,别搁那装貂 :)。
Rust 中哈希类型(哈希映射)为 `HashMap<K,V>`,在其它语言中,也有类似的数据结构,例如 `hash map``map``object``hash table``字典` 等等,引用小品演员孙涛的一句台词:大家都是本地狐狸,别搁那装貂 :)。
## 创建HashMap
@ -21,20 +21,20 @@ my_gems.insert("蓝宝石", 2);
my_gems.insert("河边捡的误以为是宝石的破石头", 18);
```
很简单对吧?跟其它语言没有区别,聪明的同学甚至能够猜到该 `HashMap` 的类型: `HashMap<&str,i32>`
很简单对吧?跟其它语言没有区别,聪明的同学甚至能够猜到该 `HashMap` 的类型:`HashMap<&str,i32>`。
但是还有一点,你可能没有注意,那就是使用 `HashMap` 需要手动通过 `use ...` 从标准库中引入到我们当前的作用域中来,仔细回忆下,之前使用另外两个集合类型 `String` 和` Vec` 时,我们是否有手动引用过?答案是 `No`,因为 `HashMap` 并没有包含在Rust的[`prelude`](../../appendix/prelude.md)中(Rust为了简化用户使用提前将最常用的类型自动引入到作用域中)
但是还有一点,你可能没有注意,那就是使用 `HashMap` 需要手动通过 `use ...` 从标准库中引入到我们当前的作用域中来,仔细回忆下,之前使用另外两个集合类型 `String` 和` Vec` 时,我们是否有手动引用过?答案是 `No`,因为 `HashMap` 并没有包含在 Rust 的 [`prelude`](../../appendix/prelude.md) 中Rust为了简化用户使用提前将最常用的类型自动引入到作用域中
所有的集合类型都是动态的,意味着它们没有固定的内存大小,因此它们底层的数据都存储在内存堆上,然后通过一个存储在栈中的引用类型来访问。同时,跟其它集合类型一致,`HashMap` 也是内聚性的,即所有的`K`必须拥有同样的类型,`V`也是如此。
所有的集合类型都是动态的,意味着它们没有固定的内存大小,因此它们底层的数据都存储在内存堆上,然后通过一个存储在栈中的引用类型来访问。同时,跟其它集合类型一致,`HashMap` 也是内聚性的,即所有的 `K` 必须拥有同样的类型,`V` 也是如此。
> 跟Vec一样如果预先知道要存储的KV对个数可以使用 `HashMap::with_capacity(capacity)` 创建指定大小的HashMap避免频繁的内存分配和拷贝提升性能
> 跟 `Vec` 一样,如果预先知道要存储的 `KV` 对个数,可以使用 `HashMap::with_capacity(capacity)` 创建指定大小的 `HashMap`,避免频繁的内存分配和拷贝,提升性能
#### 使用迭代器和collect方法创建
在实际使用中,不是所有的场景都能 `new` 一个哈希表后,然后悠哉悠哉的依次插入对应的键值对,而是可能会从另外一个数据结构中,获取到对应的数据,最终生成 `HashMap`
例如考虑一个场景有一张表格中记录了足球联赛中各队伍名称和积分的信息这张表如果被导入到Rust项目中一个合理的数据结构是 `Vec<(String,u32)>` 类型,该数组中的元素是一个个元组,该数据结构跟表格数据非常契合:表格中的数据都是逐行存储,每一个行都存有一个 `(队伍名称,积分)` 的信息。
例如考虑一个场景,有一张表格中记录了足球联赛中各队伍名称和积分的信息,这张表如果被导入到 Rust 项目中,一个合理的数据结构是 `Vec<(String, u32)>` 类型,该数组中的元素是一个个元组,该数据结构跟表格数据非常契合:表格中的数据都是逐行存储,每一个行都存有一个 `(队伍名称, 积分)` 的信息。
但是在很多时候,又需要通过队伍名称来查询对应的积分,此时动态数组就不适用了,因此可以用 `HashMap` 来保存相关的**队伍名称 -> 积分**映射关系。 理想很骨感,现实很丰满,如何将 `Vec<(String, u32)>` 中的数据快速写入到`HashMap<String, u32>`中?
但是在很多时候,又需要通过队伍名称来查询对应的积分,此时动态数组就不适用了,因此可以用 `HashMap` 来保存相关的**队伍名称 -> 积分**映射关系。 理想很骨感,现实很丰满,如何将 `Vec<(String, u32)>` 中的数据快速写入到 `HashMap<String, u32>` 中?
一个动动脚趾头就能想到的笨方法如下:
```rust
@ -56,9 +56,9 @@ fn main() {
}
```
遍历列表,将每一个元组作为一对 `KV `插入到 `HashMap` 中,很简单,但是。。。也不太聪明的样子,换个词说就是 - 不够`rusty`
遍历列表,将每一个元组作为一对 `KV `插入到 `HashMap` 中,很简单,但是……也不太聪明的样子,换个词说就是 —— 不够 rusty
好在Rust为我们提供了一个非常精妙的解决办法先将 `Vec` 转为迭代器,接着通过 `collect` 方法,将迭代器中的元素收集后,转成 `HashMap`
好在Rust 为我们提供了一个非常精妙的解决办法:先将 `Vec` 转为迭代器,接着通过 `collect` 方法,将迭代器中的元素收集后,转成 `HashMap`
```rust
fn main() {
use std::collections::HashMap;
@ -77,20 +77,20 @@ fn main() {
代码很简单,`into_iter` 方法将列表转为迭代器,接着通过 `collect` 进行收集,不过需要注意的是,`collect` 方法在内部实际上支持生成多种类型的目标集合,因为我们需要通过类型标注 `HashMap<_,_>` 来告诉编译器:请帮我们收集为 `HashMap` 集合类型,具体的 `KV` 类型,麻烦编译器您老人家帮我们推导。
由此可见Rust中的编译器时而小聪明时而大聪明不过好在它大聪明的时候会自家人知道自己事总归会通知你一声
由此可见Rust 中的编译器时而小聪明,时而大聪明,不过好在,它大聪明的时候,会自家人知道自己事,总归会通知你一声:
```console
error[E0282]: type annotations needed 需要类型标注
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一个具体的类型
| ^^^^^^^^^ consider giving `teams_map` a type // 给予 `teams_map` 一个具体的类型
```
## 所有权转移
`HashMap` 的所有权规则与其它 Rust 类型没有区别:
- 若类型实现 `Copy` 特征,该类型会被复制进 `HashMap`,因此无所谓所有权
- 若没实现 `Copy` 特征,所有权将被转移给`HashMap`中
- 若没实现 `Copy` 特征,所有权将被转移给 `HashMap`
例如我参选帅气男孩时的场景再现:
```rust
@ -240,7 +240,7 @@ for word in text.split_whitespace() {
println!("{:?}", map);
```
上面代码中,新建一个 `map` 用于保存词语出现的次数插入一个词语时会进行判断若之前没有插入过则使用该词语作Key插入次数0作为Value若之前插入过则取出之前统计的该词语出现的次数对其加一。
上面代码中,新建一个 `map` 用于保存词语出现的次数,插入一个词语时会进行判断:若之前没有插入过,则使用该词语作 `Key`,插入次数 0 作为 `Value`,若之前插入过则取出之前统计的该词语出现的次数,对其加一。
有两点值得注意:
- `or_insert` 返回了 `&mut v` 引用,因此可以通过该可变引用直接修改 `map` 中对应的值
@ -250,19 +250,19 @@ println!("{:?}", map);
## 哈希函数
你肯定比较好奇,为何叫哈希表,到底什么是哈希。
先来设想下,如果要实现 `Key``Value` 的一一对应,是不是意味着我们要能比较两个 `Key` 的相等性?例如"a"和"b"1和2当这些类型做Key且能比较时可以很容易知道 `1` 对应的值不会错误的映射到 `2` 上,因为 `1` 不等于 `2`。因此,一个类型能否作为 `Key` 的关键就是是否能进行相等比较,或者说该类型是否实现了 `std::cmp::Eq` 特征。
先来设想下,如果要实现 `Key``Value` 的一一对应,是不是意味着我们要能比较两个 `Key` 的相等性?例如 "a" "b"1 2当这些类型做 `Key` 且能比较时,可以很容易知道 `1` 对应的值不会错误的映射到 `2` 上,因为 `1` 不等于 `2`。因此,一个类型能否作为 `Key` 的关键就是是否能进行相等比较,或者说该类型是否实现了 `std::cmp::Eq` 特征。
> f32和f64浮点数没有实现 `std::cmp::Eq` 特征,因此不可以用作 `HashMap``Key`
> f32 f64 浮点数,没有实现 `std::cmp::Eq` 特征,因此不可以用作 `HashMap``Key`
好了理解完这个再来设想一点若一个复杂点的类型作为Key那怎么在底层对它进行存储怎么使用它进行查询和比较 是不是很棘手?好在我们有哈希函数:通过它把 `Key` 计算后映射为哈希值,然后使用该哈希值来进行存储、查询、比较等操作。
好了,理解完这个,再来设想一点,若一个复杂点的类型作为 `Key`,那怎么在底层对它进行存储,怎么使用它进行查询和比较? 是不是很棘手?好在我们有哈希函数:通过它把 `Key` 计算后映射为哈希值,然后使用该哈希值来进行存储、查询、比较等操作。
但是问题又来了,如何保证不同 `Key` 通过哈希后的两个值不会相同?如果相同,那意味着我们使用不同的 `Key`,却查到了同一个结果,这种明显是错误的行为。
此时,就涉及到安全性跟性能的取舍了。
若要追求安全,尽可能减少冲突,同时防止拒绝服务(Denial of Service, DoS)攻击,就要使用密码学安全的哈希函数,`HashMap` 就是使用了这样的哈希函数。反之若要追求性能,就需要使用没有那么安全的算法。
若要追求安全,尽可能减少冲突,同时防止拒绝服务Denial of Service, DoS攻击,就要使用密码学安全的哈希函数,`HashMap` 就是使用了这样的哈希函数。反之若要追求性能,就需要使用没有那么安全的算法。
#### 高性能三方库
因此若性能测试显示当前标准库默认的哈希函数不能满足你的性能需求,就需要去[`crates.io`](https://crates.io)上寻找其它的哈希函数实现,使用方法很简单:
因此若性能测试显示当前标准库默认的哈希函数不能满足你的性能需求,就需要去 [`crates.io`](https://crates.io) 上寻找其它的哈希函数实现,使用方法很简单:
```rust
use std::hash::BuildHasherDefault;
use std::collections::HashMap;

@ -1,8 +1,8 @@
# 集合类型
在Rust标准库中有这样一批原住民它们天生贵族当你看到的一瞬间就能爱上它们上面是我瞎编的其实主要是离了它们不行不信等会我介绍后你放个狠话非它们不用试试
Rust 标准库中有这样一批原住民,它们天生贵族,当你看到的一瞬间,就能爱上它们,上面是我瞎编的,其实主要是离了它们不行,不信等会我介绍后,你放个狠话,非它们不用试试?
集合在Rust中是一类比较特殊的类型因为Rust中大多数数据类型都只能代表一个特定的值但是集合却可以代表一大堆值。而且与语言级别的数组、字符串类型不同标准库里的这些家伙是分配在堆上因此都可以进行动态的增加和减少。
集合在 Rust 中是一类比较特殊的类型,因为 Rust 中大多数数据类型都只能代表一个特定的值,但是集合却可以代表一大堆值。而且与语言级别的数组、字符串类型不同,标准库里的这些家伙是分配在堆上,因此都可以进行动态的增加和减少。
瞧,第一个集合排着整体的队列登场了,它里面的每个元素都雄赳赳气昂昂跟在另外一个元素后面,大小、宽度、高度竟然全部一致,真是令人惊叹。 它就是 `Vector` 类型,允许你创建一个动态数组,它里面的元素是一个紧挨着另一个排列的。

@ -7,7 +7,7 @@
总之,当我们想拥有一个列表,里面都是相同类型的数据时,动态数组将会非常有用。
## 创建动态数组
在Rust中有多种方式可以创建动态数组。
Rust 中,有多种方式可以创建动态数组。
#### Vec::new
使用 `Vec::new` 创建动态数组是最 rusty 的方式,它调用了 `Vec` 中的 `new` 关联函数:
@ -31,7 +31,7 @@ v.push(1);
let v = vec![1, 2, 3];
```
同样,此处的 `v` 也无需标注类型,编译器只需检查它内部的元素即可自动推导出 `v` 的类型是 `Vec<i32>` (Rust中整数默认类型是i32在[数值类型](../base-type/numbers.md#整数类型)中有详细介绍)
同样,此处的 `v` 也无需标注类型,编译器只需检查它内部的元素即可自动推导出 `v` 的类型是 `Vec<i32>` Rust 中,整数默认类型是 `i32`,在[数值类型](../base-type/numbers.md#整数类型)中有详细介绍)
## 更新Vector
向数组尾部添加元素,可以使用 `push` 方法:
@ -98,7 +98,7 @@ v.push(6);
println!("The first element is: {}", first);
```
先不运行,来推断下结果,首先 `first = &v[0]` 进行了不可变借用,`v.push` 进行了可变借用,如果 `first``v.push` 之后不再使用,那么该段代码可以成功编译(原因见[引用的作用域](../ownership/borrowing.md#可变引用与不可变引用不能同时存在))
先不运行,来推断下结果,首先 `first = &v[0]` 进行了不可变借用,`v.push` 进行了可变借用,如果 `first``v.push` 之后不再使用,那么该段代码可以成功编译原因见[引用的作用域](../ownership/borrowing.md#可变引用与不可变引用不能同时存在)
可是上面的代码中,`first` 这个不可变借用在可变借用 `v.push` 后被使用了,那么妥妥的,编译器就会报错:
```console
@ -122,15 +122,15 @@ error: could not compile `collections` due to previous error
其实,按理来说,这两个引用不应该互相影响的:一个是查询元素,一个是在数组尾部插入元素,完全不相干的操作,为何编译器要这么严格呢?
原因在于数组的大小是可变的当旧数组的大小不够用时Rust会重新分配一块更大的内存空间然后把旧数组拷贝过来。这种情况下之前的引用显然会指向一块无效的内存这非常rusty - 对用户进行严格的教育。
原因在于数组的大小是可变的当旧数组的大小不够用时Rust 会重新分配一块更大的内存空间,然后把旧数组拷贝过来。这种情况下,之前的引用显然会指向一块无效的内存,这非常 rusty —— 对用户进行严格的教育。
其实想想,**在长大之后,我们感激人生路上遇到过的严师益友,正是因为他们,我们才在正确的道路上不断前行,虽然在那个时候,并不能理解他们**,而 Rust 就如那个良师益友,它不断的在纠正我们不好的编程习惯,直到某一天,你发现自己能写出一次性通过的漂亮代码时,就能明白它的良苦用心。
> 若读者想要更深入的了解`Vec<T>`,可以看看[Rustonomicon],其中从零手撸一个动态数组,非常适合深入学习
> 若读者想要更深入的了解`Vec<T>`,可以看看[Rustonomicon](https://nomicon.purewhite.io/vec/vec.html),其中从零手撸一个动态数组,非常适合深入学习
## 迭代遍历Vector中的元素
如果想要依次访问数组中的元素,可以使用迭代的方式去遍历数组,这种方式比用下标的方式去遍历数组更安全也更高效(每次下标访问都会触发数组边界检查)
如果想要依次访问数组中的元素,可以使用迭代的方式去遍历数组,这种方式比用下标的方式去遍历数组更安全也更高效(每次下标访问都会触发数组边界检查)
```rust
let v = vec![1, 2, 3];
for i in &v {
@ -205,6 +205,6 @@ fn main() {
比枚举实现要稍微复杂一些,我们为 `V4``V6` 都实现了特征 `IpAddr`,然后将它俩的实例用 `Box::new` 包裹后,存在了数组 `v` 中,需要注意的是,这里必需手动的指定类型:`Vec<Box<dyn IpAddr>>`,表示数组 `v` 存储的是特征 `IpAddr` 的对象,这样就实现了在数组中存储不同的类型。
在实际使用场景中,特征对象数组要比枚举数组常见很多,主要原因在于[特征对象非常灵活](../trait/trait-object.md),而编译器对枚举的限制较多,且无法动态增加类型。
在实际使用场景中,特征对象数组要比枚举数组常见很多,主要原因在于[特征对象](../trait/trait-object.md)非常灵活,而编译器对枚举的限制较多,且无法动态增加类型。
最后,如果你想要了解 `Vector `更多的用法,请参见本书的标准库解析章节:[`Vector`常用方法](../../std/vector.md)

Loading…
Cancel
Save