mirror of
https://github.com/KaiserY/trpl-zh-cn
synced 2024-11-09 00:43:59 +08:00
commit
00a0f285f3
@ -4,7 +4,7 @@
|
||||
> <br>
|
||||
> commit 4f2dc564851dc04b271a2260c834643dfd86c724
|
||||
|
||||
最后要介绍的常用集合类型是**哈希 map**(*hash map*)。`HashMap<K, V>`类型储存了一个键类型`K`对应一个值类型`V`的映射。它通过一个**哈希函数**(*hashing function*)来实现映射,它决定了如何将键和值放入内存中。很多编程语言支持这种数据结构,不过通常有不同的名字:哈希、map、对象、哈希表或者关联数组,仅举几例。
|
||||
最后介绍的常用集合类型是 **哈希 map**(*hash map*)。`HashMap<K, V>` 类型储存了一个键类型 `K` 对应一个值类型 `V` 的映射。它通过一个**哈希函数**(*hashing function*)来实现映射,决定如何将键和值放入内存中。很多编程语言支持这种数据结构,不过通常有不同的名字:哈希、map、对象、哈希表或者关联数组,仅举几例。
|
||||
|
||||
哈希 map 可以用于需要任何类型作为键来寻找数据的情况,而不是像 vector 那样通过索引。例如,在一个游戏中,你可以将每个团队的分数记录到哈希 map 中,其中键是队伍的名字而值是每个队伍的分数。给出一个队名,就能得到他们的得分。
|
||||
|
||||
@ -23,11 +23,10 @@ scores.insert(String::from("Blue"), 10);
|
||||
scores.insert(String::from("Yellow"), 50);
|
||||
```
|
||||
|
||||
注意必须首先`use`标准库中集合部分的`HashMap`。在这三个常用集合中,这个是最不常用的,所以并不包含在被 prelude 自动引用的功能中。标准库中对哈希 map 的支持也相对较少;例如,并没有内建的用于构建的宏。
|
||||
注意必须首先 `use` 标准库中集合部分的 `HashMap`。在这三个常用集合中,`HashMap` 是最不常用的,所以并没有被 prelude 自动引用。标准库中对 `HashMap` 的支持也相对较少,例如,并没有内建的构建宏。
|
||||
像 vector 一样,哈希 map 将他们的数据储存在堆上,这个 `HashMap` 的键类型是 `String` 而值类型是 `i32`。同样类似于 vector,哈希 map 是同质的:所有的键必须是相同类型,值也必须都是相同类型。
|
||||
|
||||
就像 vector 一样,哈希 map 将他们的数据储存在堆上。这个`HashMap`的键类型是`String`而值类型是`i32`。同样类似于 vector,哈希 map 是同质的:所有的键必须是相同类型,值也必须都是相同类型。
|
||||
|
||||
另一个构建哈希 map 的方法是使用一个元组的 vector 的`collect`方法,其中每个元组包含一个键值对。`collect`方法可以将数据收集进一系列的集合类型,包括`HashMap`。例如,如果队伍的名字和初始分数分别在两个 vector 中,可以使用`zip`方法来创建一个元组的 vector,其中“Blue”与 10 是一对,依此类推。接着就可以使用`collect`方法将这个元组 vector 转换成一个`HashMap`:
|
||||
另一个构建哈希 map 的方法是使用一个元组的 vector 的 `collect` 方法,其中每个元组包含一个键值对。`collect` 方法可以将数据收集进一系列的集合类型,包括 `HashMap`。例如,如果队伍的名字和初始分数分别在两个 vector 中,可以使用 `zip` 方法来创建一个元组的 vector,其中“Blue”与 10 是一对,依此类推。接着就可以使用 `collect` 方法将这个元组 vector 转换成一个 `HashMap`:
|
||||
|
||||
```rust
|
||||
use std::collections::HashMap;
|
||||
@ -38,7 +37,7 @@ let initial_scores = vec![10, 50];
|
||||
let scores: HashMap<_, _> = teams.iter().zip(initial_scores.iter()).collect();
|
||||
```
|
||||
|
||||
这里`HashMap<_, _>`类型注解是必要的,因为可能`collect`进很多不同的数据结构,而除非显式指定 Rust 无从得知你需要的类型。但是对于键和值的参数来说,可以使用下划线而 Rust 可以根据 vector 中数据的类型推断出哈希 map 所包含的类型。
|
||||
这里`HashMap<_, _>`类型注解是必要的,因为可能`collect`进很多不同的数据结构,而除非显式指定 Rust 无从得知你需要的类型。但是对于键和值的类型参数来说,可以使用下划线占位,而 Rust 能够根据 vector 中数据的类型推断出 `HashMap` 所包含的类型。
|
||||
|
||||
### 哈希 map 和所有权
|
||||
|
||||
@ -75,7 +74,7 @@ let team_name = String::from("Blue");
|
||||
let score = scores.get(&team_name);
|
||||
```
|
||||
|
||||
这里,`score`将会是与蓝队分数相关的值,而这个值将是`Some(10)`。因为`get`返回`Option<V>`所以结果被封装进`Some`;如果某个键在哈希 map 中没有对应的值,`get`会返回`None`。程序将需要采用第六章提到的方法中之一来处理`Option`。
|
||||
这里,`score` 是与蓝队分数相关的值,应为 `Some(10)`。因为 `get` 返回 `Option<V>`,所以结果被装进 `Some`;如果某个键在哈希 map 中没有对应的值,`get` 会返回 `None`。这时就要用某种第六章提到的方法来处理 `Option`。
|
||||
|
||||
可以使用与 vector 类似的方式来遍历哈希 map 中的每一个键值对,也就是`for`循环:
|
||||
|
||||
@ -101,8 +100,7 @@ Blue: 10
|
||||
|
||||
### 更新哈希 map
|
||||
|
||||
虽然键值对的数量是可以增长的,不过每个单独的键同时只能关联一个值。当你想要改变哈希 map 中的数据时,必须选择是用新值替代旧值,还是完全无视旧值。我们也可以选择保留旧值而忽略新值,并只在键**没有**对应一个值时增加新值。或者可以结合新值和旧值。让我们看看着每一种方式是如何工作的!
|
||||
|
||||
尽管键值对的数量是可以增长的,不过任何时候,每个键只能关联一个值。当你想要改变哈希 map 中的数据时,根据目标键是否有值以及值的更新策略分成多种情况,下面我们了解一下:
|
||||
#### 覆盖一个值
|
||||
|
||||
如果我们插入了一个键值对,接着用相同的键插入一个不同的值,与这个键相关联的旧值将被替换。即便下面的代码调用了两次`insert`,哈希 map 也只会包含一个键值对,因为两次都是对蓝队的键插入的值:
|
||||
@ -160,11 +158,11 @@ for word in text.split_whitespace() {
|
||||
println!("{:?}", map);
|
||||
```
|
||||
|
||||
这会打印出`{"world": 2, "hello": 1, "wonderful": 1}`,`or_insert`方法事实上会返回这个键的值的一个可变引用(`&mut V`)。这里我们将这个可变引用储存在`count`变量中,所以为了赋值必须首先使用星号(`*`)解引用`count`。这个可变引用在`for`循环的结尾离开作用域,这样所有这些改变都是安全的并被借用规则所允许。
|
||||
这会打印出`{"world": 2, "hello": 1, "wonderful": 1}`,`or_insert`方法事实上会返回这个键的值的一个可变引用(`&mut V`)。这里我们将这个可变引用储存在`count`变量中,所以为了赋值必须首先使用星号(`*`)解引用`count`。这个可变引用在`for`循环的结尾离开作用域,这样所有这些改变都是安全的并符合借用规则。
|
||||
|
||||
### 哈希函数
|
||||
|
||||
`HashMap`默认使用一个密码学上是安全的哈希函数,它可以提供抵抗拒绝服务(Denial of Service, DoS)攻击的能力。这并不是现有最快的哈希函数,不过为了更好的安全性带来一些性能下降也是值得的。如果你监控你的代码并发现默认哈希函数对你来说非常慢,可以通过指定一个不同的 *hasher* 来切换为另一个函数。hasher 是一个实现了`BuildHasher` trait 的类型。第十章会讨论 trait 和如何实现他们。你并不需要从头开始实现你自己的 hasher;crates.io 有其他人分享的实现了许多常用哈希算法的 hasher 的库。
|
||||
`HashMap`默认使用一种密码学安全的哈希函数,它可以抵抗拒绝服务(Denial of Service, DoS)攻击。然而并不是最快的,不过为了更高的安全性值得付出一些性能的代价。如果性能监测显示此哈希函数非常慢,以致于你无法接受,你可以指定一个不同的 *hasher* 来切换为其它函数。hasher 是一个实现了`BuildHasher` trait 的类型。第十章会讨论 trait 和如何实现他们。你并不需要从头开始实现你自己的 hasher;crates.io 有其他人分享的实现了许多常用哈希算法的 hasher 的库。
|
||||
|
||||
## 总结
|
||||
|
||||
|
@ -11,24 +11,24 @@
|
||||
>
|
||||
> 不要共享内存来通讯;而是要通讯来共享内存。
|
||||
|
||||
那么“共享内存来通讯”看起来是怎样的呢?共享内存并发有点像多所有权:多个线程可以同时访问相同的内存位置。正如第十五章中智能指针使得多所有权成为可能时我们所看到的,这会增加额外的复杂性,因为需要以某种方式管理这些不同的所有者。
|
||||
那么“共享内存来通讯”是怎样的呢?共享内存并发有点像多所有权:多个线程可以同时访问相同的内存位置。第十五章介绍了智能指针如何使得多所有权成为可能,然而这会增加额外的复杂性,因为需要以某种方式管理这些不同的所有者。
|
||||
|
||||
但是 Rust 的类型系统和所有权可以很好的帮助我们正确的进行管理。例如,让我们看看一个共享内存中更常见的并发原语:互斥器(mutexes)。
|
||||
不过 Rust 的类型系统和所有权可以很好的帮助我们,正确的管理它们。以共享内存中更常见的并发原语:互斥器(mutexes)为例,让我们看看具体的情况。
|
||||
|
||||
### 互斥器一次只允许一个线程访问数据
|
||||
|
||||
**互斥器**(*mutex*)是一个用于共享内存的并发原语。它是“mutual exclusion”的缩写,也就是说,任何给定时间它只允许一个线程访问某些数据。互斥器以难以使用著称,因为你不得不记住:
|
||||
**互斥器**(*mutex*)是一种用于共享内存的并发原语。它是“mutual exclusion”的缩写,也就是说,任意时间,它只允许一个线程访问某些数据。互斥器以难以使用著称,因为你不得不记住:
|
||||
|
||||
1. 必须记住在使用数据之前尝试获取锁。
|
||||
2. 一旦处理完被互斥器所保护的数据之后,必须记得解锁数据这样其他线程才能够获取锁。
|
||||
1. 在使用数据之前尝试获取锁。
|
||||
2. 处理完被互斥器所保护的数据之后,必须解锁数据,这样其他线程才能够获取锁。
|
||||
|
||||
对于一个现实中的互斥器的例子,想象一下在一个会议中的专门小组讨论会上,不过只有一个麦克风。在一个小组成员可能发言之前,他们必须请求或示意他们需要使用麦克风。一旦得到了麦克风,他们可以发言任意长的时间,接着将麦克风交给下一个希望讲话的小组成员。如果小组成员在没有麦克风的时候就开始叫喊或者在其他成员发言结束之前就取得麦克风将是很无理的。如果对这个共享的麦克风的管理因为任何这些原因而出现问题,讨论会将无法如期进行。
|
||||
现实中也有互斥器的例子,想象一下在一个会议中,只有一个麦克风。如果一个成员要发言,他必须请求使用麦克风。一旦得到了麦克风,他可以畅所欲言,然后将麦克风交给下一个希望讲话的成员。如果成员在没有麦克风的时候就开始叫喊,或者在其他成员发言结束之前就拿走麦克风,是很不合适的。如果这个共享的麦克风因为此类原因而出现问题,会议将无法正常进行。
|
||||
|
||||
正确的管理互斥器是异常复杂的,这也就是为什么这么多人都热衷于通道。然而,在 Rust 中,得益于类型系统和所有权,我们不可能会在锁和解锁上出错。
|
||||
正确的管理互斥器异常复杂,这也是许多人之所以热衷于通道的原因。然而,在 Rust 中,得益于类型系统和所有权,我们不会在锁和解锁上出错。
|
||||
|
||||
### `Mutex<T>`的 API
|
||||
|
||||
让我们看看列表 16-12 中使用互斥器的例子,现在并不涉及到多线程:
|
||||
让我们看看列表 16-12 中使用互斥器的例子,现在不涉及多线程:
|
||||
|
||||
<span class="filename">Filename: src/main.rs</span>
|
||||
|
||||
@ -50,10 +50,12 @@ fn main() {
|
||||
<span class="caption">Listing 16-12: Exploring the API of `Mutex<T>` in a
|
||||
single threaded context for simplicity</span>
|
||||
|
||||
与很多类型一样,我们通过叫做`new`的关联函数来创建一个`Mutex<T>`。为了访问互斥器中的数据,使用`lock`方法来获取锁。这个调用会阻塞到直到轮到我们拥有锁为止。如果另一个线程拥有锁接着那个线程 panic 了则这个调用会失败。类似于上一部分列表 16-6 那样,我们暂时使用`unwrap()`,至于更好的错误处理,请查看第九章中提供的更好的工具。
|
||||
像很多类型一样,我们使用关联函数 `new` 来创建一个 `Mutex<T>`。使用`lock`方法获取锁,以访问互斥器中的数据。这个调用会阻塞,直到我们拥有锁为止。如果另一个线程拥有锁,并且那个线程 panic 了,则这个调用会失败。类似于列表 16-6 那样,我们暂时使用 `unwrap()` 进行错误处理,或者使用第九章中提及的更好的工具。
|
||||
|
||||
|
||||
一旦获取了锁,就可以将返回值(在这里是`num`)作为一个数据的可变引用使用了。观察 Rust 类型系统如何保证使用值之前必须获取锁:`Mutex<i32>`并不是一个`i32`,所以**必须**获取锁才能使用这个`i32`值。我们是不会忘记这么做的,因为类型系统不允许。
|
||||
|
||||
|
||||
你也许会怀疑,`Mutex<T>`是一个智能指针?是的!更准确的说,`lock`调用返回一个叫做`MutexGuard`的智能指针。类似我们在第十五章见过的智能指针,它实现了`Deref`来指向其内部数据。另外`MutexGuard`有一个用来释放锁的`Drop`实现。这样就不会忘记释放锁了。这在`MutexGuard`离开作用域时会自动发生,例如它发生于列表 16-12 中内部作用域的结尾。接着可以打印出互斥器的值并发现能够将其内部的`i32`改为 6。
|
||||
|
||||
#### 在线程间共享`Mutex<T>`
|
||||
|
Loading…
Reference in New Issue
Block a user