Update: unified format

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

@ -1,6 +1,6 @@
# KV存储HashMap # 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``字典` 等等,引用小品演员孙涛的一句台词:大家都是本地狐狸,别搁那装貂 :)。
@ -23,11 +23,11 @@ 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方法创建 #### 使用迭代器和collect方法创建
在实际使用中,不是所有的场景都能 `new` 一个哈希表后,然后悠哉悠哉的依次插入对应的键值对,而是可能会从另外一个数据结构中,获取到对应的数据,最终生成 `HashMap` 在实际使用中,不是所有的场景都能 `new` 一个哈希表后,然后悠哉悠哉的依次插入对应的键值对,而是可能会从另外一个数据结构中,获取到对应的数据,最终生成 `HashMap`
@ -56,7 +56,7 @@ fn main() {
} }
``` ```
遍历列表,将每一个元组作为一对 `KV `插入到 `HashMap` 中,很简单,但是。。。也不太聪明的样子,换个词说就是 - 不够`rusty` 遍历列表,将每一个元组作为一对 `KV `插入到 `HashMap` 中,很简单,但是……也不太聪明的样子,换个词说就是 —— 不够 rusty
好在Rust 为我们提供了一个非常精妙的解决办法:先将 `Vec` 转为迭代器,接着通过 `collect` 方法,将迭代器中的元素收集后,转成 `HashMap` 好在Rust 为我们提供了一个非常精妙的解决办法:先将 `Vec` 转为迭代器,接着通过 `collect` 方法,将迭代器中的元素收集后,转成 `HashMap`
```rust ```rust
@ -79,11 +79,11 @@ fn main() {
由此可见Rust 中的编译器时而小聪明,时而大聪明,不过好在,它大聪明的时候,会自家人知道自己事,总归会通知你一声: 由此可见Rust 中的编译器时而小聪明,时而大聪明,不过好在,它大聪明的时候,会自家人知道自己事,总归会通知你一声:
```console ```console
error[E0282]: type annotations needed 需要类型标注 error[E0282]: type annotations needed // 需要类型标注
--> src/main.rs:10:9 --> src/main.rs:10:9
| |
10 | let teams_map = teams_list.into_iter().collect(); 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` 一个具体的类型
``` ```
## 所有权转移 ## 所有权转移
@ -240,7 +240,7 @@ for word in text.split_whitespace() {
println!("{:?}", map); println!("{:?}", map);
``` ```
上面代码中,新建一个 `map` 用于保存词语出现的次数插入一个词语时会进行判断若之前没有插入过则使用该词语作Key插入次数0作为Value若之前插入过则取出之前统计的该词语出现的次数对其加一。 上面代码中,新建一个 `map` 用于保存词语出现的次数,插入一个词语时会进行判断:若之前没有插入过,则使用该词语作 `Key`,插入次数 0 作为 `Value`,若之前插入过则取出之前统计的该词语出现的次数,对其加一。
有两点值得注意: 有两点值得注意:
- `or_insert` 返回了 `&mut v` 引用,因此可以通过该可变引用直接修改 `map` 中对应的值 - `or_insert` 返回了 `&mut v` 引用,因此可以通过该可变引用直接修改 `map` 中对应的值
@ -250,16 +250,16 @@ 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`,却查到了同一个结果,这种明显是错误的行为。 但是问题又来了,如何保证不同 `Key` 通过哈希后的两个值不会相同?如果相同,那意味着我们使用不同的 `Key`,却查到了同一个结果,这种明显是错误的行为。
此时,就涉及到安全性跟性能的取舍了。 此时,就涉及到安全性跟性能的取舍了。
若要追求安全,尽可能减少冲突,同时防止拒绝服务(Denial of Service, DoS)攻击,就要使用密码学安全的哈希函数,`HashMap` 就是使用了这样的哈希函数。反之若要追求性能,就需要使用没有那么安全的算法。 若要追求安全,尽可能减少冲突,同时防止拒绝服务Denial of Service, DoS攻击,就要使用密码学安全的哈希函数,`HashMap` 就是使用了这样的哈希函数。反之若要追求性能,就需要使用没有那么安全的算法。
#### 高性能三方库 #### 高性能三方库
因此若性能测试显示当前标准库默认的哈希函数不能满足你的性能需求,就需要去 [`crates.io`](https://crates.io) 上寻找其它的哈希函数实现,使用方法很简单: 因此若性能测试显示当前标准库默认的哈希函数不能满足你的性能需求,就需要去 [`crates.io`](https://crates.io) 上寻找其它的哈希函数实现,使用方法很简单:

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

Loading…
Cancel
Save