mirror of
https://github.com/KaiserY/trpl-zh-cn
synced 2024-11-09 08:51:18 +08:00
check ch08-03
This commit is contained in:
parent
2c5097b783
commit
c5a42db0bf
@ -1,15 +1,15 @@
|
||||
# 模块
|
||||
# 使用模块组织和复用代码
|
||||
|
||||
> [ch07-00-modules.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch07-00-modules.md)
|
||||
> <br>
|
||||
> commit 4f2dc564851dc04b271a2260c834643dfd86c724
|
||||
> commit b707dc664960f0ffc495c373900d6b13e434927d
|
||||
|
||||
在你刚开始编写 Rust 程序时,代码可能仅仅位于`main`函数里。随着代码数量的增长,最终你会将功能移动到其他函数中,为了复用也为了更好的组织。通过将代码分隔成更小的块,每一个块代码自身就更易于理解。不过当你发现自己有太多的函数了该怎么办呢?Rust 有一个模块系统来处理编写可复用代码同时保持代码组织度的问题。
|
||||
在你刚开始编写 Rust 程序时,代码可能仅仅位于 `main` 函数里。随着代码数量的增长,最终你会将功能移动到其他函数中,为了复用也为了更好的组织。通过将代码分隔成更小的块,每一个块代码自身就更易于理解。不过当你发现自己有太多的函数了该怎么办呢?Rust 有一个模块系统可以有组织的复用代码。
|
||||
|
||||
就跟你将代码行提取到一个函数中一样,也可以将函数(和其他类似结构体和枚举的代码)提取到不同模块中。**模块**(*module*)是一个包含函数或类型定义的命名空间,你可以选择这些定义是能(公有)还是不能(私有)在其模块外可见。这是一个模块如何工作的概括:
|
||||
|
||||
* 使用`mod`关键字声明模块
|
||||
* 默认所有内容都是私有的(包括模块自身)。可以使用`pub`关键字将其变成公有并在其命名空间外可见。
|
||||
* `use`关键字允许引入模块、或模块中的定义到作用域中以便于引用他们。
|
||||
* 使用 `mod` 关键字声明新模块。此模块的代码要么直接位于声明之后的大括号中,要么位于另一个文件。
|
||||
* 函数、类型、常量和模块默认都是私有的。可以使用 `pub` 关键字将其变成公有并在命名空间之外可见。
|
||||
* `use` 关键字引入模块、或模块中的定义到作用域中以便于引用他们。
|
||||
|
||||
我们会逐一了解这每一部分并学习如何将他们结合在一起。
|
@ -1,21 +1,21 @@
|
||||
## `mod`和文件系统
|
||||
## `mod` 和文件系统
|
||||
|
||||
> [ch07-01-mod-and-the-filesystem.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch07-01-mod-and-the-filesystem.md)
|
||||
> <br>
|
||||
> commit b0481ac44ff2594c6c240baa36357737739db445
|
||||
> commit c6a9e77a1b1ed367e0a6d5dcd222589ad392a8ac
|
||||
|
||||
我们将通过使用 Cargo 创建一个新项目来开始我们的模块之旅,不过不再创建一个二进制 crate,而是创建一个库 crate:一个其他人可以作为依赖导入的项目。第二章我们见过的`rand`就是这样的 crate。
|
||||
我们将通过使用 Cargo 创建一个新项目来开始我们的模块之旅,不过不再创建一个二进制 crate,而是创建一个库 crate:一个其他人可以作为依赖导入的项目。第二章猜猜看游戏中作为依赖使用的 `rand` 就是这样的 crate。
|
||||
|
||||
我们将创建一个提供一些通用网络功能的项目的骨架结构;我们将专注于模块和函数的组织,而不担心函数体中的具体代码。这个项目叫做`communicator`。Cargo 默认会创建一个库 crate 除非指定其他项目类型,所以如果不像一直以来那样加入`--bin`参数则项目将会是一个库:
|
||||
我们将创建一个提供一些通用网络功能的项目的骨架结构;我们将专注于模块和函数的组织,而不担心函数体中的具体代码。这个项目叫做 `communicator`。Cargo 默认会创建一个库 crate 除非指定其他项目类型,所以如果不像一直以来那样加入 `--bin` 参数则项目将会是一个库:
|
||||
|
||||
```
|
||||
```text
|
||||
$ cargo new communicator
|
||||
$ cd communicator
|
||||
```
|
||||
|
||||
注意 Cargo 生成了 *src/lib.rs* 而不是 *src/main.rs*。在 *src/lib.rs* 中我们会找到这些:
|
||||
|
||||
<span class="filename">Filename: src/lib.rs</span>
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
@ -26,17 +26,17 @@ mod tests {
|
||||
}
|
||||
```
|
||||
|
||||
Cargo 创建了一个空的测试来帮助我们开始库项目,不像使用`--bin`参数那样创建一个“Hello, world!”二进制项目。稍后一点会介绍`#[]`和`mod tests`语法,目前只需确保他们位于 *src/lib.rs* 中。
|
||||
Cargo 创建了一个空的测试来帮助我们开始库项目,不像使用 `--bin` 参数那样创建一个 “Hello, world!” 二进制项目。在本章之后的 “使用 `super` 访问父模块” 部分会介绍 `#[]` 和 `mod tests` 语法,目前只需确保他们位于 *src/lib.rs* 底部即可。
|
||||
|
||||
因为没有 *src/main.rs* 文件,所以没有可供 Cargo 的`cargo run`执行的东西。因此,我们将使用`cargo build`命令只是编译库 crate 的代码。
|
||||
因为没有 *src/main.rs* 文件,所以没有可供 Cargo 的 `cargo run` 执行的东西。因此,我们将使用 `cargo build` 命令只是编译库 crate 的代码。
|
||||
|
||||
我们将学习根据编写代码的意图来选择不同的织库项目代码组织来适应多种场景。
|
||||
|
||||
### 模块定义
|
||||
|
||||
对于`communicator`网络库,首先要定义一个叫做`network`的模块,它包含一个叫做`connect`的函数定义。Rust 中所有模块的定义以关键字`mod`开始。在 *src/lib.rs* 文件的开头在测试代码的上面增加这些代码:
|
||||
对于 `communicator` 网络库,首先要定义一个叫做 `network` 的模块,它包含一个叫做 `connect` 的函数定义。Rust 中所有模块的定义以关键字 `mod` 开始。在 *src/lib.rs* 文件的开头在测试代码的上面增加这些代码:
|
||||
|
||||
<span class="filename">Filename: src/lib.rs</span>
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
```rust
|
||||
mod network {
|
||||
@ -45,11 +45,11 @@ mod network {
|
||||
}
|
||||
```
|
||||
|
||||
`mod`关键字的后面是模块的名字,`network`,接着是位于大括号中的代码块。代码块中的一切都位于`network`命名空间中。在这个例子中,只有一个函数,`connect`。如果想要在`network`模块外面的代码中调用这个函数,需要指定模块名并使用命名空间语法`::`,像这样:`network::connect()`,而不是只是`connect()`。
|
||||
`mod` 关键字的后面是模块的名字,`network`,接着是位于大括号中的代码块。代码块中的一切都位于 `network` 命名空间中。在这个例子中,只有一个函数,`connect`。如果想要在 `network` 模块外面的代码中调用这个函数,需要指定模块名并使用命名空间语法 `::`,像这样:`network::connect()`,而不是只是 `connect()`。
|
||||
|
||||
也可以在 *src/lib.rs* 文件中同时存在多个模块。例如,再拥有一个`client`模块,它也有一个叫做`connect`的函数,如列表 7-1 中所示那样增加这个模块:
|
||||
也可以在 *src/lib.rs* 文件中同时存在多个模块。例如,再拥有一个 `client` 模块,它也有一个叫做 `connect` 的函数,如列表 7-1 中所示那样增加这个模块:
|
||||
|
||||
<span class="filename">Filename: src/lib.rs</span>
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
```rust
|
||||
mod network {
|
||||
@ -63,14 +63,13 @@ mod client {
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">Listing 7-1: The `network` module and the `client` module
|
||||
defined side-by-side in *src/lib.rs*</span>
|
||||
<span class="caption">列表 7-1:`network` 模块和 `client` 一同定义于 *src/lib.rs*</span>
|
||||
|
||||
现在我们有了`network::connect`函数和`client::connect`函数。他们可能有着完全不同的功能,同时他们也不会彼此冲突,因为他们位于不同的模块。
|
||||
现在我们有了 `network::connect` 函数和 `client::connect` 函数。他们可能有着完全不同的功能,同时他们也不会彼此冲突,因为他们位于不同的模块。
|
||||
|
||||
虽然在这个例子中,我们构建了一个库,但是 *src/lib.rs* 并没有什么特殊意义。也可以在 *src/main.rs* 中使用子模块。事实上,也可以将模块放入其他模块中。这有助于随着模块的增长,将相关的功能组织在一起并又保持各自独立。如何选择组织代码依赖于如何考虑代码不同部分之间的关系。例如,对于库的用户来说,`client`模块和它的函数`connect`可能放在`network`命名空间里显得更有道理,如列表 7-2 所示:
|
||||
在这个例子中,因为我们构建的是一个库,作为库入口点的文件是 *src/lib.rs*。然而,对于创建模块来说,*src/lib.rs* 并没有什么特殊意义。也可以在二进制 crate 的 *src/main.rs* 中创建模块,正如在库 crate 的 *src/lib.rs* 创建模块一样。事实上,也可以将模块放入其他模块中。这有助于随着模块的增长,将相关的功能组织在一起并又保持各自独立。如何选择组织代码依赖于如何考虑代码不同部分之间的关系。例如,对于库的用户来说,`client` 模块和它的函数 `connect` 可能放在 `network` 命名空间里显得更有道理,如列表 7-2 所示:
|
||||
|
||||
<span class="filename">Filename: src/lib.rs</span>
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
```rust
|
||||
mod network {
|
||||
@ -84,34 +83,33 @@ mod network {
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">Listing 7-2: Moving the `client` module inside of the
|
||||
`network` module</span>
|
||||
<span class="caption">列表 7-2:将 `client` 模块移动到 `network` 模块中</span>
|
||||
|
||||
在 *src/lib.rs* 文件中,将现有的`mod network`和`mod client`的定义替换为`client`模块作为`network`的一个内部模块。现在我们有了`network::connect`和`network::client::connect`函数:又一次,这两个`connect`函数也不相冲突,因为他们在不同的命名空间中。
|
||||
在 *src/lib.rs* 文件中,将现有的 `mod network` 和 `mod client` 的定义替换为列表 7-2 中的定义,这里将 `client` 模块作为 `network` 的一个内部模块。现在我们有了 `network::connect` 和 `network::client::connect` 函数:同样的,这两个 `connect` 函数也不相冲突,因为他们在不同的命名空间中。
|
||||
|
||||
这样,模块之间形成了一个层次结构。*src/lib.rs* 的内容位于最顶层,而其子模块位于较低的层次。这是列表 7-1 中的例子以这种方式考虑的组织结构:
|
||||
这样,模块之间形成了一个层次结构。*src/lib.rs* 的内容位于最顶层,而其子模块位于较低的层次。如下是列表 7-1 中的例子以层次的方式考虑的结构:
|
||||
|
||||
```
|
||||
```text
|
||||
communicator
|
||||
├── network
|
||||
└── client
|
||||
```
|
||||
|
||||
而这是列表 7-2 中例子的的结构:
|
||||
而这是列表 7-2 中例子的的层次结构:
|
||||
|
||||
```
|
||||
```text
|
||||
communicator
|
||||
└── network
|
||||
└── client
|
||||
```
|
||||
|
||||
可以看到列表 7-2 中,`client`是`network`的子模块,而不是它的同级模块。更为负责的项目可以有很多的模块,所以他们需要符合逻辑地组合在一起以便记录他们。在项目中“符合逻辑”的意义全凭你的理解和库的用户对你项目领域的认识。利用我们这里讲到的技术来创建同级模块和嵌套的模块将是你会喜欢的结构。
|
||||
可以看到列表 7-2 中,`client` 是 `network` 的子模块,而不是它的同级模块。更为复杂的项目可以有很多的模块,所以他们需要符合逻辑地组合在一起以便记录他们。在项目中 “符合逻辑” 的意义全凭你的理解和库的用户对你项目领域的认识。利用我们这里讲到的技术来创建同级模块和嵌套的模块,总有一个会是你会喜欢的结构。
|
||||
|
||||
### 将模块移动到其他文件
|
||||
|
||||
位于层级结构中的模块,非常类似计算机领域的另一个我们非常熟悉的结构:文件系统!我们可以利用 Rust 的模块系统连同多个文件一起分解 Rust 项目,这样就不是所有的内容都落到 *src/lib.rs* 中了。作为例子,我们将从列表 7-3 中的代码开始:
|
||||
位于层级结构中的模块,非常类似计算机领域的另一个我们非常熟悉的结构:文件系统!我们可以利用 Rust 的模块系统连同多个文件一起分解 Rust 项目,这样就不会是所有的内容都落到 *src/lib.rs* 或 *src/main.rs* 中了。为了举例,我们将从列表 7-3 中的代码开始:
|
||||
|
||||
<span class="filename">Filename: src/lib.rs</span>
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
```rust
|
||||
mod client {
|
||||
@ -130,23 +128,22 @@ mod network {
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">Listing 7-3: Three modules, `client`, `network`, and
|
||||
`network::server`, all defined in *src/lib.rs*</span>
|
||||
<span class="caption">列表 7-3:三个模块,`client`、`network` 和 `network::server`,他们都定义于 *src/lib.rs*</span>
|
||||
|
||||
这是模块层次结构:
|
||||
*src/lib.rs* 文件有如下层次结构:
|
||||
|
||||
```
|
||||
```text
|
||||
communicator
|
||||
├── client
|
||||
└── network
|
||||
└── server
|
||||
```
|
||||
|
||||
如果这些模块有很多函数,而这些函数又很长,将难以在文件中寻找我们需要的代码。因为这些函数被嵌套进一个或多个模块中,同时函数中的代码也会开始变长。这就有充分的理由将`client`、`network`和`server`每一个模块从 *src/lib.rs* 抽出并放入他们自己的文件中。
|
||||
如果这些模块有很多函数,而这些函数又很长,将难以在文件中寻找我们需要的代码。因为这些函数被嵌套进一个或多个模块中,同时函数中的代码也会开始变长。这就有充分的理由将`client`、`network` 和 `server`每一个模块从 *src/lib.rs* 抽出并放入他们自己的文件中。
|
||||
|
||||
让我们开始把`client`模块提取到另一个文件中。首先,将 *src/lib.rs* 中的`client`模块代码替换为如下:
|
||||
首先,将 `client` 模块的代码替换为只有 `client` 模块声明,这样 *src/lib.rs* 看起来应该像这样:
|
||||
|
||||
<span class="filename">Filename: src/lib.rs</span>
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
```rust,ignore
|
||||
mod client;
|
||||
@ -162,9 +159,15 @@ mod network {
|
||||
}
|
||||
```
|
||||
|
||||
这里我们仍然**定义**了`client`模块,不过去掉了大括号和`client`模块中的定义并替换为一个分号,这使得 Rust 知道去其他地方寻找模块中定义的代码。
|
||||
这里我们仍然 **声明** 了 `client` 模块,不过将代码块替换为了分号,这告诉了 Rust 在 `client` 模块的作用域中寻找另一个定义代码的位置。换句话说,`mod client;` 行意味着:
|
||||
|
||||
那么现在需要创建对应模块名的外部文件。在 *src/* 目录创建一个 *client.rs* 文件,接着打开它并输入如下内容,它是上一步`client`模块中被去掉的`connect`函数:
|
||||
```rust,ignore
|
||||
mod client {
|
||||
// contents of client.rs
|
||||
}
|
||||
```
|
||||
|
||||
那么现在需要创建对应模块名的外部文件。在 *src/* 目录创建一个 *client.rs* 文件,接着打开它并输入如下内容,它是上一步被去掉的 `client` 模块中的 `connect` 函数:
|
||||
|
||||
<span class="filename">Filename: src/client.rs</span>
|
||||
|
||||
@ -173,13 +176,13 @@ fn connect() {
|
||||
}
|
||||
```
|
||||
|
||||
注意这个文件中并不需要一个`mod`声明;因为已经在 *src/lib.rs* 中已经使用`mod`声明了`client`模块。这个文件仅仅提供`client`模块的**内容**。如果在这里加上一个`mod client`,那么就等于给`client`模块增加了一个叫做`client`的子模块了!
|
||||
注意这个文件中并不需要一个 `mod` 声明;因为已经在 *src/lib.rs* 中已经使用 `mod` 声明了 `client` 模块。这个文件仅仅提供 `client` 模块的 **内容**。如果在这里加上一个 `mod client`,那么就等于给 `client` 模块增加了一个叫做 `client` 的子模块了!
|
||||
|
||||
Rust 默认只知道 *src/lib.rs* 中的内容。如果想要对项目加入更多文件,我们需要在 *src/lib.rs* 中告诉 Rust 去寻找其他文件;这就是为什么`mod client`需要被定义在 *src/lib.rs* 而不是在 *src/client.rs*。
|
||||
Rust 默认只知道 *src/lib.rs* 中的内容。如果想要对项目加入更多文件,我们需要在 *src/lib.rs* 中告诉 Rust 去寻找其他文件;这就是为什么 `mod client` 需要被定义在 *src/lib.rs* 而不能在 *src/client.rs* 的原因。
|
||||
|
||||
现在,一切应该能成功编译,虽然会有一些警告。记住使用`cargo build`而不是`cargo run`因为这是一个库 crate 而不是二进制 crate:
|
||||
现在,一切应该能成功编译,虽然会有一些警告。记住使用 `cargo build` 而不是 `cargo run` 因为这是一个库 crate 而不是二进制 crate:
|
||||
|
||||
```
|
||||
```text
|
||||
$ cargo build
|
||||
Compiling communicator v0.1.0 (file:///projects/communicator)
|
||||
|
||||
@ -202,9 +205,9 @@ warning: function is never used: `connect`, #[warn(dead_code)] on by default
|
||||
| ^
|
||||
```
|
||||
|
||||
这些警告提醒我们有从未被使用的函数。目前不用担心这些警告;在本章的后面会解决他们。好消息是,他们仅仅是警告;我们的项目能够被成功编译。
|
||||
这些警告提醒我们有从未被使用的函数。目前不用担心这些警告;在本章后面的 “使用 `pub` 控制可见性” 部分会解决他们。好消息是,他们仅仅是警告;我们的项目能够被成功编译。
|
||||
|
||||
下面使用相同的模式将`network`模块提取到它自己的文件中。删除 *src/lib.rs* 中`network`模块的内容并在声明后加上一个分号,像这样:
|
||||
下面使用相同的模式将 `network` 模块提取到自己的文件中。删除 *src/lib.rs* 中 `network` 模块的内容并在声明后加上一个分号,像这样:
|
||||
|
||||
<span class="filename">Filename: src/lib.rs</span>
|
||||
|
||||
@ -228,9 +231,9 @@ mod server {
|
||||
}
|
||||
```
|
||||
|
||||
注意这个模块文件中我们也使用了一个`mod`声明;这是因为我们希望`server`成为`network`的一个子模块。
|
||||
注意这个模块文件中我们也使用了一个 `mod` 声明;这是因为我们希望 `server` 成为 `network` 的一个子模块。
|
||||
|
||||
现在再次运行`cargo build`。成功!不过我们还需要再提取出另一个模块:`server`。因为这是一个子模块——也就是模块中的模块——目前的将模块提取到对应名字的文件中的策略就不管用了。如果我们仍这么尝试则会出现错误。对 *src/network.rs* 的第一个修改是用`mod server;`替换`server`模块的内容:
|
||||
现在再次运行 `cargo build`。成功!不过我们还需要再提取出另一个模块:`server`。因为这是一个子模块——也就是模块中的模块——目前的将模块提取到对应名字的文件中的策略就不管用了。如果我们仍这么尝试则会出现错误。对 *src/network.rs* 的第一个修改是用 `mod server;` 替换 `server` 模块的内容:
|
||||
|
||||
<span class="filename">Filename: src/network.rs</span>
|
||||
|
||||
@ -241,7 +244,7 @@ fn connect() {
|
||||
mod server;
|
||||
```
|
||||
|
||||
接着创建 *src/server.rs* 文件并输入需要提取的`server`模块的内容:
|
||||
接着创建 *src/server.rs* 文件并输入需要提取的 `server` 模块的内容:
|
||||
|
||||
<span class="filename">Filename: src/server.rs</span>
|
||||
|
||||
@ -250,7 +253,7 @@ fn connect() {
|
||||
}
|
||||
```
|
||||
|
||||
当尝试运行`cargo build`时,会出现如列表 7-4 中所示的错误:
|
||||
当尝试运行 `cargo build` 时,会出现如列表 7-4 中所示的错误:
|
||||
|
||||
```text
|
||||
$ cargo build
|
||||
@ -273,34 +276,34 @@ note: ... or maybe `use` the module `server` instead of possibly redeclaring it
|
||||
| ^^^^^^
|
||||
```
|
||||
|
||||
<span class="caption">Listing 7-4: Error when trying to extract the `server`
|
||||
submodule into *src/server.rs*</span>
|
||||
<span class="caption">列表 7-4:尝试将 `server` 子模块提取到 *src/server.rs* 时出现的错误</span>
|
||||
|
||||
这个错误说明“不能在这个位置新声明一个模块”并指出 *src/network.rs* 中的`mod server;`这一行。看来 *src/network.rs* 与 *src/lib.rs* 在某些方面是不同的;让我们继续阅读以理解这是为什么。
|
||||
这个错误说明 “不能在这个位置新声明一个模块” 并指出 *src/network.rs* 中的 `mod server;` 这一行。看来 *src/network.rs* 与 *src/lib.rs* 在某些方面是不同的;继续阅读以理解这是为什么。
|
||||
|
||||
列表 7-4 中间的记录事实上是非常有帮助的,因为它指出了一些我们还未讲到的操作:
|
||||
列表 7-4 中间的 note 事实上是非常有帮助的,因为它指出了一些我们还未讲到的操作:
|
||||
|
||||
```
|
||||
note: maybe move this module `network` to its own directory via `network/mod.rs`
|
||||
```text
|
||||
note: maybe move this module `network` to its own directory via
|
||||
`network/mod.rs`
|
||||
```
|
||||
|
||||
我们可以按照记录所建议的去操作,而不是继续使用之前的与模块同名的文件的模式:
|
||||
我们可以按照记录所建议的去操作,而不是继续使用之前的与模块同名文件的模式:
|
||||
|
||||
1. 新建一个叫做 *network* 的**目录**,这是父模块的名字
|
||||
1. 新建一个叫做 *network* 的 **目录**,这是父模块的名字
|
||||
2. 将 *src/network.rs* 移动到新建的 *network* 目录中并重命名,现在它是 *src/network/mod.rs*
|
||||
3. 将子模块文件 *src/server.rs* 移动到 *network* 目录中
|
||||
|
||||
如下是执行这些步骤的命令:
|
||||
|
||||
```sh
|
||||
```text
|
||||
$ mkdir src/network
|
||||
$ mv src/network.rs src/network/mod.rs
|
||||
$ mv src/server.rs src/network
|
||||
```
|
||||
|
||||
现在如果运行`cargo build`的话将顺利编译(虽然仍有警告)。现在模块的布局看起来仍然与列表 7-3 中所有代码都在 *src/lib.rs* 中时完全一样:
|
||||
现在如果运行 `cargo build` 的话将顺利编译(虽然仍有警告)。现在模块的布局看起来仍然与列表 7-3 中所有代码都在 *src/lib.rs* 中时完全一样:
|
||||
|
||||
```
|
||||
```text
|
||||
communicator
|
||||
├── client
|
||||
└── network
|
||||
@ -309,7 +312,7 @@ communicator
|
||||
|
||||
对应的文件布局现在看起来像这样:
|
||||
|
||||
```
|
||||
```text
|
||||
├── src
|
||||
│ ├── client.rs
|
||||
│ ├── lib.rs
|
||||
@ -318,34 +321,34 @@ communicator
|
||||
│ └── server.rs
|
||||
```
|
||||
|
||||
那么,当我们想要提取`network::server`模块时,为什么也必须将 *src/network.rs* 文件改名成 *src/network/mod.rs* 文件呢,还有为什么要将`network::server`的代码放入 *network* 目录的 *src/network/server.rs* 文件中,而不能将`network::server`模块提取到 *src/server.rs* 中呢?原因是如果 *server.rs* 文件在 *src* 目录中那么 Rust 就不能知道`server`应当是`network`的子模块。为了更清楚得说明为什么 Rust 不知道,让我们考虑一下有着如下层级的另一个例子,它的所有定义都位于 *src/lib.rs* 中:
|
||||
那么,当我们想要提取 `network::server` 模块时,为什么也必须将 *src/network.rs* 文件改名成 *src/network/mod.rs* 文件呢,还有为什么要将`network::server`的代码放入 *network* 目录的 *src/network/server.rs* 文件中,而不能将 `network::server` 模块提取到 *src/server.rs* 中呢?原因是如果 *server.rs* 文件在 *src* 目录中那么 Rust 就不能知道 `server` 应当是 `network` 的子模块。为了阐明这里 Rust 的行为,让我们考虑一下有着如下层级的另一个例子,它的所有定义都位于 *src/lib.rs* 中:
|
||||
|
||||
```
|
||||
```text
|
||||
communicator
|
||||
├── client
|
||||
└── network
|
||||
└── client
|
||||
```
|
||||
|
||||
在这个例子中,仍然有这三个模块,`client`、`network`和`network::client`。如果按照与上面最开始将模块提取到文件中相同的步骤来操作,对于`client`模块会创建 *src/client.rs*。对于`network`模块,会创建 *src/network.rs*。但是接下来不能将`network::client`模块提取到 *src/client.rs* 文件中,因为它已经存在了,对应顶层的`client`模块!如果将`client`和`network::client`的代码都放入 *src/client.rs* 文件,Rust 将无从可知这些代码是属于`client`还是`network::client`的。
|
||||
在这个例子中,仍然有这三个模块,`client`、`network` 和 `network::client`。如果按照与上面最开始将模块提取到文件中相同的步骤来操作,对于 `client` 模块会创建 *src/client.rs*。对于 `network` 模块,会创建 *src/network.rs*。但是接下来不能将 `network::client` 模块提取到 *src/client.rs* 文件中,因为它已经存在了,对应顶层的 `client` 模块!如果将 `client` 和 `network::client` 的代码都放入 *src/client.rs* 文件,Rust 将无从可知这些代码是属于 `client` 还是 `network::client` 的。
|
||||
|
||||
因此,一旦想要将`network`模块的子模块`network::client`提取到一个文件中,需要为`network`模块新建一个目录替代 *src/network.rs* 文件。接着`network`模块的代码将进入 *src/network/mod.rs* 文件,而子模块`network::client`将拥有其自己的文件 *src/network/client.rs*。现在顶层的 *src/client.rs* 中的代码毫无疑问的都属于`client`模块。
|
||||
因此,一旦想要将 `network` 模块的子模块 `network::client` 提取到一个文件中,需要为 `network` 模块新建一个目录替代 *src/network.rs* 文件。接着 `network` 模块的代码将进入 *src/network/mod.rs* 文件,而子模块 `network::client` 将拥有其自己的文件 *src/network/client.rs*。现在顶层的 *src/client.rs* 中的代码毫无疑问的都属于 `client` 模块。
|
||||
|
||||
### 模块文件系统的规则
|
||||
|
||||
与文件系统相关的模块规则总结如下:
|
||||
|
||||
* 如果一个叫做`foo`的模块没有子模块,应该将`foo`的声明放入叫做 *foo.rs* 的文件中。
|
||||
* 如果一个叫做`foo`的模块有子模块,应该将`foo`的声明放入叫做 *foo/mod.rs* 的文件中。
|
||||
* 如果一个叫做 `foo` 的模块没有子模块,应该将 `foo` 的声明放入叫做 *foo.rs* 的文件中。
|
||||
* 如果一个叫做 `foo` 的模块有子模块,应该将 `foo` 的声明放入叫做 *foo/mod.rs* 的文件中。
|
||||
|
||||
这些规则适用于递归(嵌套),所以如果`foo`模块有一个子模块`bar`而`bar`没有子模块,则 *src* 目录中应该有如下文件:
|
||||
这些规则适用于递归(嵌套),所以如果 `foo` 模块有一个子模块 `bar` 而 `bar` 没有子模块,则 *src* 目录中应该有如下文件:
|
||||
|
||||
```
|
||||
```text
|
||||
├── foo
|
||||
│ ├── bar.rs (contains the declarations in `foo::bar`)
|
||||
│ └── mod.rs (contains the declarations in `foo`, including `mod bar`)
|
||||
```
|
||||
|
||||
模块自身则应该使用`mod`关键字定义于父模块的文件中。
|
||||
模块自身则应该使用 `mod` 关键字定义于父模块的文件中。
|
||||
|
||||
接下来,我们讨论一下`pub`关键字,并除掉那些警告!
|
||||
接下来,我们讨论一下 `pub` 关键字,并除掉那些警告!
|
||||
|
@ -1,12 +1,12 @@
|
||||
## 使用`pub`控制可见性
|
||||
## 使用 `pub` 控制可见性
|
||||
|
||||
> [ch07-02-controlling-visibility-with-pub.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch07-02-controlling-visibility-with-pub.md)
|
||||
> <br>
|
||||
> commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56
|
||||
> commit 0a4ed5875aeba78a81ae03ac73aeb84d2e2aca86
|
||||
|
||||
我们通过将`network`和`network::server`的代码分别移动到 *src/network/mod.rs* 和 *src/network/server.rs* 文件中解决了列表 7-4 中出现的错误信息。现在,`cargo build`能够构建我们的项目,不过仍然有一些警告信息,表示`client::connect`、`network::connect`和`network::server::connect`函数没有被使用:
|
||||
我们通过将 `network` 和 `network::server` 的代码分别移动到 *src/network/mod.rs* 和 *src/network/server.rs* 文件中解决了列表 7-4 中出现的错误信息。现在,`cargo build` 能够构建我们的项目,不过仍然有一些警告信息,表示 `client::connect`、`network::connect` 和`network::server::connect` 函数没有被使用:
|
||||
|
||||
```
|
||||
```text
|
||||
warning: function is never used: `connect`, #[warn(dead_code)] on by default
|
||||
src/client.rs:1:1
|
||||
|
|
||||
@ -26,11 +26,11 @@ warning: function is never used: `connect`, #[warn(dead_code)] on by default
|
||||
| ^
|
||||
```
|
||||
|
||||
那么为什么会出现这些错误信息呢?我们构建的是一个库,它的函数的目的是被**用户**使用,而不一定要被项目自身使用,所以不应该担心这些`connect`函数是未使用的。创建他们的意义就在于被另一个项目而不是被自己使用。
|
||||
那么为什么会出现这些错误信息呢?我们构建的是一个库,它的函数的目的是被 **用户** 使用,而不一定要被项目自身使用,所以不应该担心这些 `connect` 函数是未使用的。创建他们的意义就在于被另一个项目而不是被我们自己使用。
|
||||
|
||||
为了理解为什么这个程序出现了这些警告,尝试作为另一个项目来使用这个`connect`库,从外部调用他们。为此,通过创建一个包含这些代码的 *src/main.rs* 文件,在与库 crate 相同的目录创建一个二进制 crate:
|
||||
为了理解为什么这个程序出现了这些警告,尝试作为另一个项目来使用这个 `connect` 库,从外部调用他们。为此,通过创建一个包含这些代码的 *src/main.rs* 文件,在与库 crate 相同的目录创建一个二进制 crate:
|
||||
|
||||
<span class="filename">Filename: src/main.rs</span>
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
```rust,ignore
|
||||
extern crate communicator;
|
||||
@ -40,15 +40,15 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
使用`extern crate`指令将`communicator`库 crate 引入到作用域,因为事实上我们的包包含**两个** crate。Cargo 认为 *src/main.rs* 是一个二进制 crate 的根文件,与现存的以 *src/lib.rs* 为根文件的库 crate 相区分。这个模式在可执行项目中非常常见:大部分功能位于库 crate 中,而二进制 crate 使用这个库 crate。通过这种方式,其他程序也可以使用这个库 crate,这是一个很好的关注分离(separation of concerns)。
|
||||
使用 `extern crate` 指令将 `communicator` 库 crate 引入到作用域,因为事实上我们的包现在包含 **两个** crate。Cargo 认为 *src/main.rs* 是一个二进制 crate 的根文件,与现存的以 *src/lib.rs* 为根文件的库 crate 相区分。这个模式在可执行项目中非常常见:大部分功能位于库 crate 中,而二进制 crate 使用这个库 crate。通过这种方式,其他程序也可以使用这个库 crate,这是一个很好的关注分离(separation of concerns)。
|
||||
|
||||
从一个外部 crate 的视角观察`communicator`库的内部,我们创建的所有模块都位于一个与 crate 同名的模块内部,`communicator`。这个顶层的模块被称为 crate 的**根模块**(*root module*)。
|
||||
从一个外部 crate 的视角观察 `communicator` 库的内部,我们创建的所有模块都位于一个与 crate 同名的模块内部,`communicator`。这个顶层的模块被称为 crate 的 **根模块**(*root module*)。
|
||||
|
||||
另外注意到即便在项目的子模块中使用外部 crate,`extern crate`也应该位于根模块(也就是 *src/main.rs* 或 *src/lib.rs*)。接着,在子模块中,我们就可以像顶层模块那样引用外部 crate 中的项了。
|
||||
另外注意到即便在项目的子模块中使用外部 crate,`extern crate` 也应该位于根模块(也就是 *src/main.rs* 或 *src/lib.rs*)。接着,在子模块中,我们就可以像顶层模块那样引用外部 crate 中的项了。
|
||||
|
||||
我们的二进制 crate 如今正好调用了库中`client`模块的`connect`函数。然而,执行`cargo build`会在之前的警告之后出现一个错误:
|
||||
我们的二进制 crate 如今正好调用了库中 `client` 模块的 `connect` 函数。然而,执行 `cargo build` 会在之前的警告之后出现一个错误:
|
||||
|
||||
```
|
||||
```text
|
||||
error: module `client` is private
|
||||
--> src/main.rs:4:5
|
||||
|
|
||||
@ -56,15 +56,15 @@ error: module `client` is private
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
```
|
||||
|
||||
啊哈!这告诉了我们`client`模块是私有的,这也正是那些警告的症结所在。这也是我们第一次在 Rust 上下文中涉及到**公有**和**私有**的概念。Rust 所有代码的默认状态是私有的:除了自己之外别人不允许使用这些代码。如果不在自己的项目中使用一个私有函数,因为程序自身是唯一允许使用这个函数的代码,Rust 会警告说函数未被使用。
|
||||
啊哈!这告诉了我们 `client` 模块是私有的,这也正是那些警告的症结所在。这也是我们第一次在 Rust 上下文中涉及到 **公有**(*public*)和 **私有**(*private*)的概念。Rust 所有代码的默认状态是私有的:除了自己之外别人不允许使用这些代码。如果不在自己的项目中使用一个私有函数,因为程序自身是唯一允许使用这个函数的代码,Rust 会警告说函数未被使用。
|
||||
|
||||
一旦我们指定一个像`client::connect`的函数为公有,不光二进制 crate 中的函数调用是允许的,函数未被使用的警告也会消失。将其标记为公有让 Rust 知道了我们意在使函数在我们程序的外部被使用。现在这个可能的理论上的外部可用性使得 Rust 认为这个函数“已经被使用”。因此。当某项被标记为公有,Rust 不再要求它在程序自身被使用并停止警告某项未被使用。
|
||||
一旦我们指定一个像 `client::connect` 的函数为公有,不光二进制 crate 中的函数调用是允许的,函数未被使用的警告也会消失。将其标记为公有让 Rust 知道了我们意在使函数在程序的外部被使用。现在这个可能的理论上的外部可用性使得 Rust 认为这个函数 “已经被使用”。因此。当某项被标记为公有,Rust 不再要求它在程序自身被使用并停止警告某项未被使用。
|
||||
|
||||
### 标记函数为公有
|
||||
|
||||
为了告诉 Rust 某项为公有,在想要标记为公有的项的声明开头加上`pub`关键字。现在我们将致力于修复`client::connect`未被使用的警告,以及二进制 crate 中“模块`client`是私有的”的错误。像这样修改 *src/lib.rs* 使`client`模块公有:
|
||||
为了告诉 Rust 某项为公有,在想要标记为公有的项的声明开头加上 `pub` 关键字。现在我们将致力于修复 `client::connect` 未被使用的警告,以及二进制 crate 中 “模块`client`是私有的” 的错误。像这样修改 *src/lib.rs* 使 `client` 模块公有:
|
||||
|
||||
<span class="filename">Filename: src/lib.rs</span>
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
```rust,ignore
|
||||
pub mod client;
|
||||
@ -72,10 +72,9 @@ pub mod client;
|
||||
mod network;
|
||||
```
|
||||
|
||||
`pub`写在`mod`之前。再次尝试构建:
|
||||
`pub` 写在 `mod` 之前。再次尝试构建:
|
||||
|
||||
```
|
||||
<warnings>
|
||||
```text
|
||||
error: function `connect` is private
|
||||
--> src/main.rs:4:5
|
||||
|
|
||||
@ -83,18 +82,18 @@ error: function `connect` is private
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
```
|
||||
|
||||
非常好!另一个不同的错误!好的,不同的错误信息是值得庆祝的(可能是程序员被黑的最惨的一次)。新错误表明“函数`connect`是私有的”,那么让我们修改 *src/client.rs* 将`client::connect`也设为公有:
|
||||
非常好!另一个不同的错误!好的,不同的错误信息也是值得庆祝的(可能是程序员被黑的最惨的一次)。新错误表明 “函数 `connect` 是私有的”,那么让我们修改 *src/client.rs* 将 `client::connect` 也设为公有:
|
||||
|
||||
<span class="filename">Filename: src/client.rs</span>
|
||||
<span class="filename">文件名: src/client.rs</span>
|
||||
|
||||
```rust
|
||||
pub fn connect() {
|
||||
}
|
||||
```
|
||||
|
||||
再再一次运行`cargo build`:
|
||||
再一次运行 `cargo build`:
|
||||
|
||||
```
|
||||
```text
|
||||
warning: function is never used: `connect`, #[warn(dead_code)] on by default
|
||||
--> src/network/mod.rs:1:1
|
||||
|
|
||||
@ -108,13 +107,13 @@ warning: function is never used: `connect`, #[warn(dead_code)] on by default
|
||||
| ^
|
||||
```
|
||||
|
||||
编译通过了,关于`client::connect`未被使用的警告消失了!
|
||||
编译通过了,关于 `client::connect` 未被使用的警告消失了!
|
||||
|
||||
未被使用的代码并不总是意味着他们需要被设为公有的:如果你**不**希望这些函数成为公有 API 的一部分,未被使用的代码警告可能是在警告你这些代码不再需要并可以安全的删除他们。这也可能是警告你出 bug,如果你刚刚不小心删除了库中所有这个函数的调用。
|
||||
未被使用的代码并不总是意味着他们需要被设为公有的:如果你 **不** 希望这些函数成为公有 API 的一部分,未被使用的代码警告可能是在警告你这些代码不再需要并可以安全的删除他们。这也可能是警告你出 bug 了,如果你刚刚不小心删除了库中所有这个函数的调用。
|
||||
|
||||
当然我们的情况是,**确实**希望另外两个函数也作为 crate 公有 API 的一部分,所以让我们也将其标记为`pub`并去掉剩余的警告。修改 *src/network/mod.rs* 为:
|
||||
当然我们的情况是,**确实** 希望另外两个函数也作为 crate 公有 API 的一部分,所以让我们也将其标记为 `pub` 并去掉剩余的警告。修改 *src/network/mod.rs* 为:
|
||||
|
||||
<span class="filename">Filename: src/network/mod.rs</span>
|
||||
<span class="filename">文件名: src/network/mod.rs</span>
|
||||
|
||||
```rust,ignore
|
||||
pub fn connect() {
|
||||
@ -123,9 +122,9 @@ pub fn connect() {
|
||||
mod server;
|
||||
```
|
||||
|
||||
并编译:
|
||||
并编译代码:
|
||||
|
||||
```
|
||||
```text
|
||||
warning: function is never used: `connect`, #[warn(dead_code)] on by default
|
||||
--> src/network/mod.rs:1:1
|
||||
|
|
||||
@ -139,10 +138,9 @@ warning: function is never used: `connect`, #[warn(dead_code)] on by default
|
||||
| ^
|
||||
```
|
||||
|
||||
恩,虽然将`network::connect`设为`pub`了我们仍然得到了一个未被使用函数的警告。这是因为模块中的函数是公有的,不过函数所在的`network`模块却不是公有的。这回我们是自内向外修改库文件的,而`client::connect`的时候是自外向内修改的。我们需要修改 *src/lib.rs* 让 `network` 也是公有的:
|
||||
恩,虽然将 `network::connect` 设为 `pub` 了我们仍然得到了一个未被使用函数的警告。这是因为模块中的函数是公有的,不过函数所在的 `network` 模块却不是公有的。这回我们是自内向外修改库文件的,而 `client::connect` 的时候是自外向内修改的。我们需要修改 *src/lib.rs* 让 `network` 也是公有的:
|
||||
|
||||
|
||||
<span class="filename">Filename: src/lib.rs</span>
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
```rust,ignore
|
||||
pub mod client;
|
||||
@ -150,9 +148,9 @@ pub mod client;
|
||||
pub mod network;
|
||||
```
|
||||
|
||||
现在再编译的话,那个警告就消失了:
|
||||
现在编译的话,那个警告就消失了:
|
||||
|
||||
```
|
||||
```text
|
||||
warning: function is never used: `connect`, #[warn(dead_code)] on by default
|
||||
--> src/network/server.rs:1:1
|
||||
|
|
||||
@ -167,13 +165,13 @@ warning: function is never used: `connect`, #[warn(dead_code)] on by default
|
||||
总的来说,有如下项的可见性规则:
|
||||
|
||||
1. 如果一个项是公有的,它能被任何父模块访问
|
||||
2. 如果一个项是私有的,它只能被当前模块或其子模块访问
|
||||
2. 如果一个项是私有的,它能被其直接父模块及其任何子模块访问
|
||||
|
||||
### 私有性示例
|
||||
|
||||
让我们看看更多例子作为练习。创建一个新的库项目并在新项目的 *src/lib.rs* 输入列表 7-5 中的代码:
|
||||
|
||||
<span class="filename">Filename: src/lib.rs</span>
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
```rust,ignore
|
||||
mod outermost {
|
||||
@ -196,29 +194,28 @@ fn try_me() {
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">Listing 7-5: Examples of private and public functions,
|
||||
some of which are incorrect</span>
|
||||
<span class="caption">列表 7-5:私有和公有函数的例子,其中部分是不正确的</span>
|
||||
|
||||
在尝试编译这些代码之前,猜测一下`try_me`函数的哪一行会出错。接着编译项目来看看是否猜对了,然后继续阅读后面关于错误的讨论!
|
||||
在尝试编译这些代码之前,猜测一下 `try_me` 函数的哪一行会出错。接着编译项目来看看是否猜对了,然后继续阅读后面关于错误的讨论!
|
||||
|
||||
#### 检查错误
|
||||
|
||||
`try_me`函数位于项目的根模块。叫做`outermost`的模块是私有的,不过第二条私有性规则说明`try_me`函数允许访问`outermost`模块,因为`outermost`位于当前(根)模块,`try_me`也是。
|
||||
`try_me` 函数位于项目的根模块。叫做 `outermost` 的模块是私有的,不过第二条私有性规则说明` try_me` 函数允许访问 `outermost` 模块,因为 `outermost` 位于当前(根)模块,`try_me` 也是。
|
||||
|
||||
`outermost::middle_function`的调用是正确的。因为`middle_function`是公有的,而`try_me`通过其父模块访问`middle_function`,`outermost`。根据上一段的规则我们可以确定这个模块是可访问的。
|
||||
`outermost::middle_function` 的调用是正确的。因为 `middle_function` 是公有的,而 `try_me` 通过其父模块 `outermost` 访问 `middle_function`。根据上一段的规则我们可以确定这个模块是可访问的。
|
||||
|
||||
`outermost::middle_secret_function`的调用会造成一个编译错误。`middle_secret_function`是私有的,所以第二条(私有性)规则生效了。根模块既不是`middle_secret_function`的当前模块(`outermost`是),也不是`middle_secret_function`当前模块的子模块。
|
||||
`outermost::middle_secret_function` 的调用会造成一个编译错误。`middle_secret_function` 是私有的,所以第二条(私有性)规则生效了。根模块既不是 `middle_secret_function` 的当前模块(`outermost`是),也不是 `middle_secret_function` 当前模块的子模块。
|
||||
|
||||
叫做`inside`的模块是私有的且没有子模块,所以它只能被当前模块`outermost`访问。这意味着`try_me`函数不允许调用`outermost::inside::inner_function`或`outermost::inside::secret_function`任何一个。
|
||||
叫做 `inside` 的模块是私有的且没有子模块,所以它只能被当前模块 `outermost` 访问。这意味着 `try_me` 函数不允许调用 `outermost::inside::inner_function` 或 `outermost::inside::secret_function` 中的任何一个。
|
||||
|
||||
#### 修改错误
|
||||
|
||||
这里有一些尝试修复错误的代码修改意见。在你尝试他们之前,猜测一下他们哪个能修复错误,接着编译查看你是否猜对了,并结合私有性规则理解为什么。
|
||||
|
||||
* 如果`inside`模块是公有的?
|
||||
* 如果`outermost`是公有的而`inside`是私有的?
|
||||
* 如果在`inner_function`函数体中调用`::outermost::middle_secret_function()`?(开头的两个冒号意味着从根模块开始引用模块。)
|
||||
* 如果 `inside` 模块是公有的?
|
||||
* 如果 `outermost` 是公有的而 `inside` 是私有的?
|
||||
* 如果在 `inner_function` 函数体中调用 `::outermost::middle_secret_function()`?(开头的两个冒号意味着从根模块开始引用模块。)
|
||||
|
||||
请随意设计更多的实验并尝试理解他们!
|
||||
|
||||
接下来,让我们讨论一下使用`use`关键字将模块项目引入作用域。
|
||||
接下来,让我们讨论一下使用 `use` 关键字将模块项目引入作用域。
|
@ -2,11 +2,11 @@
|
||||
|
||||
> [ch07-03-importing-names-with-use.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch07-03-importing-names-with-use.md)
|
||||
> <br>
|
||||
> commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56
|
||||
> commit d06a6a181fd61704cbf7feb55bc61d518c6469f9
|
||||
|
||||
我们已经讲到了如何使用模块名称作为调用的一部分,来调用模块中的函数,如列表 7-6 中所示的`nested_modules`函数调用。
|
||||
我们已经讲到了如何使用模块名称作为调用的一部分,来调用模块中的函数,如列表 7-6 中所示的 `nested_modules` 函数调用。
|
||||
|
||||
<span class="filename">Filename: src/main.rs</span>
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
```rust
|
||||
pub mod a {
|
||||
@ -22,16 +22,15 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
<span class="caption">Listing 7-6: Calling a function by fully specifying its
|
||||
enclosing module’s namespaces</span>
|
||||
<span class="caption">列表 7-6:通过完全指定模块中的路径来调用函数</span>
|
||||
|
||||
如你所见,指定函数的完全限定名称可能会非常冗长。所幸 Rust 有一个关键字使得这些调用显得更简洁。
|
||||
|
||||
### 使用`use`的简单导入
|
||||
### 使用 `use` 的简单导入
|
||||
|
||||
Rust 的`use`关键字的工作是缩短冗长的函数调用,通过将想要调用的函数所在的模块引入到作用域中。这是一个将`a::series::of`模块导入一个二进制 crate 的根作用域的例子:
|
||||
Rust 的 `use` 关键字的工作是缩短冗长的函数调用,通过将想要调用的函数所在的模块引入到作用域中。这是一个将 `a::series::of` 模块导入一个二进制 crate 的根作用域的例子:
|
||||
|
||||
<span class="filename">Filename: src/main.rs</span>
|
||||
<span class="filename">文件名: src/main.rs</span>
|
||||
|
||||
```rust
|
||||
pub mod a {
|
||||
@ -49,11 +48,11 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
`use a::series::of;`这一行的意思是每当想要引用`of`模块时,不用使用完整的`a::series::of`路径,可以直接使用`of`。
|
||||
`use a::series::of;` 这一行的意思是每当想要引用 `of` 模块时,不必使用完整的 `a::series::of` 路径,可以直接使用 `of`。
|
||||
|
||||
`use`关键字只将指定的模块引入作用域;它并不会将其子模块也引入。这就是为什么想要调用`nested_modules`函数时仍然必须写成`of::nested_modules`。
|
||||
`use` 关键字只将指定的模块引入作用域;它并不会将其子模块也引入。这就是为什么想要调用 `nested_modules` 函数时仍然必须写成 `of::nested_modules`。
|
||||
|
||||
也可以将函数本身引入到作用域中,通过如下在`use`中指定函数的方式:
|
||||
也可以将函数本身引入到作用域中,通过如下在 `use` 中指定函数的方式:
|
||||
|
||||
```rust
|
||||
pub mod a {
|
||||
@ -73,7 +72,7 @@ fn main() {
|
||||
|
||||
这使得我们可以忽略所有的模块并直接引用函数。
|
||||
|
||||
因为枚举也像模块一样组成了某种命名空间,也可以使用`use`来导入枚举的成员。对于任何类型的`use`语句,如果从一个命名空间导入多个项,可以使用大括号和逗号来列举他们,像这样:
|
||||
因为枚举也像模块一样组成了某种命名空间,也可以使用 `use` 来导入枚举的成员。对于任何类型的 `use` 语句,如果从一个命名空间导入多个项,可以使用大括号和逗号来列举他们,像这样:
|
||||
|
||||
```rust
|
||||
enum TrafficLight {
|
||||
@ -87,13 +86,13 @@ use TrafficLight::{Red, Yellow};
|
||||
fn main() {
|
||||
let red = Red;
|
||||
let yellow = Yellow;
|
||||
let green = TrafficLight::Green; // because we didn’t `use` TrafficLight::Green
|
||||
let green = TrafficLight::Green;
|
||||
}
|
||||
```
|
||||
|
||||
### 使用`*`的全局引用导入
|
||||
### 使用 `*` 的全局引用导入
|
||||
|
||||
为了一次导入某个命名空间的所有项,可以使用`*`语法。例如:
|
||||
为了一次导入某个命名空间的所有项,可以使用 `*` 语法。例如:
|
||||
|
||||
```rust
|
||||
enum TrafficLight {
|
||||
@ -111,13 +110,13 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
`*`被称为**全局导入**(*glob*),它会导入命名空间中所有可见的项。全局导入应该保守的使用:他们是方便的,但是也可能会引入多于你预期的内容从而导致命名冲突。
|
||||
`*` 被称为 **全局导入**(*glob*),它会导入命名空间中所有可见的项。全局导入应该保守的使用:他们是方便的,但是也可能会引入多于你预期的内容从而导致命名冲突。
|
||||
|
||||
### 使用`super`访问父模块
|
||||
### 使用 `super` 访问父模块
|
||||
|
||||
正如我们已经知道的,当创建一个库 crate 时,Cargo 会生成一个`tests`模块。现在让我们来深入了解一下。在`communicator`项目中,打开 *src/lib.rs*。
|
||||
正如我们已经知道的,当创建一个库 crate 时,Cargo 会生成一个 `tests` 模块。现在让我们来深入了解一下。在 `communicator` 项目中,打开 *src/lib.rs*。
|
||||
|
||||
<span class="filename">Filename: src/lib.rs</span>
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
```rust,ignore
|
||||
pub mod client;
|
||||
@ -132,9 +131,9 @@ mod tests {
|
||||
}
|
||||
```
|
||||
|
||||
第十一章会更详细的解释测试,不过其部分内容现在应该可以理解了:有一个叫做`tests`的模块紧邻其他模块,同时包含一个叫做`it_works`的函数。即便存在一些特殊注解,`tests`也不过是另外一个模块!所以我们的模块层次结构看起来像这样:
|
||||
第十一章会更详细的解释测试,不过其部分内容现在应该可以理解了:有一个叫做 `tests` 的模块紧邻其他模块,同时包含一个叫做 `it_works` 的函数。即便存在一些特殊注解,`tests` 也不过是另外一个模块!所以我们的模块层次结构看起来像这样:
|
||||
|
||||
```
|
||||
```text
|
||||
communicator
|
||||
├── client
|
||||
├── network
|
||||
@ -142,9 +141,9 @@ communicator
|
||||
└── tests
|
||||
```
|
||||
|
||||
测试是为了检验库中的代码而存在的,所以让我们尝试在`it_works`函数中调用`client::connect`函数,即便现在不准备测试任何功能:
|
||||
测试是为了检验库中的代码而存在的,所以让我们尝试在 `it_works` 函数中调用 `client::connect` 函数,即便现在不准备测试任何功能:
|
||||
|
||||
<span class="filename">Filename: src/lib.rs</span>
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
@ -156,9 +155,9 @@ mod tests {
|
||||
}
|
||||
```
|
||||
|
||||
使用`cargo test`命令运行测试:
|
||||
使用 `cargo test` 命令运行测试:
|
||||
|
||||
```
|
||||
```text
|
||||
$ cargo test
|
||||
Compiling communicator v0.1.0 (file:///projects/communicator)
|
||||
error[E0433]: failed to resolve. Use of undeclared type or module `client`
|
||||
@ -166,35 +165,29 @@ error[E0433]: failed to resolve. Use of undeclared type or module `client`
|
||||
|
|
||||
9 | client::connect();
|
||||
| ^^^^^^^^^^^^^^^ Use of undeclared type or module `client`
|
||||
|
||||
warning: function is never used: `connect`, #[warn(dead_code)] on by default
|
||||
--> src/network/server.rs:1:1
|
||||
|
|
||||
1 | fn connect() {
|
||||
| ^
|
||||
```
|
||||
|
||||
编译失败了,不过为什么呢?并不需要像 *src/main.rs* 那样将`communicator::`置于函数前,因为这里肯定是在`communicator`库 crate 之内的。之所以失败的原因是路径是相对于当前模块的,在这里就是`tests`。唯一的例外就是`use`语句,它默认是相对于 crate 根模块的。我们的`tests`模块需要`client`模块位于其作用域中!
|
||||
编译失败了,不过为什么呢?并不需要像 *src/main.rs* 那样将 `communicator::` 置于函数前,因为这里肯定是在 `communicator` 库 crate 之内的。之所以失败的原因是路径是相对于当前模块的,在这里就是 `tests`。唯一的例外就是 `use` 语句,它默认是相对于 crate 根模块的。我们的 `tests` 模块需要 `client` 模块位于其作用域中!
|
||||
|
||||
那么如何在模块层次结构中回退一级模块,以便在`tests`模块中能够调用`client::connect`函数呢?在`tests`模块中,要么可以在开头使用双冒号来让 Rust 知道我们想要从根模块开始并列出整个路径:
|
||||
那么如何在模块层次结构中回退一级模块,以便在 `tests` 模块中能够调用 `client::connect`函数呢?在 `tests` 模块中,要么可以在开头使用双冒号来让 Rust 知道我们想要从根模块开始并列出整个路径:
|
||||
|
||||
```rust,ignore
|
||||
::client::connect();
|
||||
```
|
||||
|
||||
要么可以使用`super`在层级中获取当前模块的上一级模块:
|
||||
要么可以使用 `super` 在层级中获取当前模块的上一级模块:
|
||||
|
||||
```rust,ignore
|
||||
super::client::connect();
|
||||
```
|
||||
|
||||
在这个例子中这两个选择看不出有多么大的区别,不过随着模块层次的更加深入,每次都从根模块开始就会显得很长了。在这些情况下,使用`super`来获取当前模块的同级模块是一个好的捷径。再加上,如果在代码中的很多地方指定了从根开始的路径,那么当通过移动子树或到其他位置来重新排列模块时,最终就需要更新很多地方的路径,这就非常乏味无趣了。
|
||||
在这个例子中这两个选择看不出有多么大的区别,不过随着模块层次的更加深入,每次都从根模块开始就会显得很长了。在这些情况下,使用 `super` 来获取当前模块的同级模块是一个好的捷径。再加上,如果在代码中的很多地方指定了从根开始的路径,那么当通过移动子树或到其他位置来重新排列模块时,最终就需要更新很多地方的路径,这就非常乏味无趣了。
|
||||
|
||||
在每一个测试中总是不得不编写`super::`也会显得很恼人,不过你已经见过解决这个问题的利器了:`use`!`super::`的功能改变了提供给`use`的路径,使其不再相对于根模块而是相对于父模块。
|
||||
在每一个测试中总是不得不编写 `super::` 也会显得很恼人,不过你已经见过解决这个问题的利器了:`use`!`super::` 的功能改变了提供给 `use` 的路径,使其不再相对于根模块而是相对于父模块。
|
||||
|
||||
为此,特别是在`tests`模块,`use super::something`是常用的手段。所以现在的测试看起来像这样:
|
||||
为此,特别是在 `tests` 模块,`use super::something` 是常用的手段。所以现在的测试看起来像这样:
|
||||
|
||||
<span class="filename">Filename: src/lib.rs</span>
|
||||
<span class="filename">文件名: src/lib.rs</span>
|
||||
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
@ -210,7 +203,7 @@ mod tests {
|
||||
|
||||
如果再次运行`cargo test`,测试将会通过而且测试结果输出的第一部分将会是:
|
||||
|
||||
```
|
||||
```text
|
||||
$ cargo test
|
||||
Compiling communicator v0.1.0 (file:///projects/communicator)
|
||||
Running target/debug/communicator-92007ddb5330fa5a
|
||||
|
@ -2,12 +2,12 @@
|
||||
|
||||
> [ch08-00-common-collections.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch08-00-common-collections.md)
|
||||
> <br>
|
||||
> commit e6d6caab41471f7115a621029bd428a812c5260e
|
||||
> commit d06a6a181fd61704cbf7feb55bc61d518c6469f9
|
||||
|
||||
Rust 标准库中包含一系列被称为**集合**(*collections*)的非常有用的数据结构。大部分其他数据类型都代表一个特定的值,不过集合可以包含多个值。不同于内建的数组和元组类型,这些集合指向的数据是储存在堆上的,这意味着数据的数量不必在编译时就可知并且可以随着程序的运行增长或缩小。每种集合都有着不同能力和代价,而为所处的场景选择合适的集合则是你将要始终发展的技能。在这一章里,我们将详细的了解三个在 Rust 程序中被广泛使用的集合:
|
||||
Rust 标准库中包含一系列被称为 **集合**(*collections*)的非常有用的数据结构。大部分其他数据类型都代表一个特定的值,不过集合可以包含多个值。不同于内建的数组和元组类型,这些集合指向的数据是储存在堆上的,这意味着数据的数量不必在编译时就可知并且可以随着程序的运行增长或缩小。每种集合都有着不同能力和代价,而为所处的场景选择合适的集合则是你将要始终发展的技能。在这一章里,我们将详细的了解三个在 Rust 程序中被广泛使用的集合:
|
||||
|
||||
* *vector* 允许我们一个挨着一个地储存一系列数量可变的值
|
||||
* **字符串**(*string*)是一个字符的集合。我们之前见过`String`类型,现在将详细介绍它。
|
||||
* **字符串**(*string*)是一个字符的集合。我们之前见过 `String` 类型,现在将详细介绍它。
|
||||
* **哈希 map**(*hash map*)允许我们将值与一个特定的键(key)相关联。这是一个叫做 *map* 的更通用的数据结构的特定实现。
|
||||
|
||||
对于标准库提供的其他类型的集合,请查看[文档][collections]。
|
||||
|
@ -8,25 +8,25 @@
|
||||
|
||||
### 新建 vector
|
||||
|
||||
为了创建一个新的,空的 vector,可以调用`Vec::new`函数:
|
||||
为了创建一个新的,空的 vector,可以调用 `Vec::new` 函数:
|
||||
|
||||
```rust
|
||||
let v: Vec<i32> = Vec::new();
|
||||
```
|
||||
|
||||
注意这里我们增加了一个类型注解。因为没有向这个 vector 中插入任何值,Rust 并不知道我们想要储存什么类型的元素。这是一个非常重要的点。vector 是同质的(homogeneous):他们可以储存很多值,不过这些值必须都是相同类型的。vector 是用泛型实现的,第十章会涉及到如何对你自己的类型使用他们。现在,所有你需要知道的就是`Vec`是一个由标准库提供的类型,它可以存放任何类型,而当`Vec`存放某个特定类型时,那个类型位于尖括号中。这里我们告诉 Rust `v`这个`Vec`将存放`i32`类型的元素。
|
||||
注意这里我们增加了一个类型注解。因为没有向这个 vector 中插入任何值,Rust 并不知道我们想要储存什么类型的元素。这是一个非常重要的点。vector 是同质的(homogeneous):他们可以储存很多值,不过这些值必须都是相同类型的。vector 是用泛型实现的,第十章会涉及到如何对你自己的类型使用他们。现在,所有你需要知道的就是 `Vec` 是一个由标准库提供的类型,它可以存放任何类型,而当 `Vec` 存放某个特定类型时,那个类型位于尖括号中。这里我们告诉 Rust `v` 这个 `Vec` 将存放 `i32` 类型的元素。
|
||||
|
||||
在实际的代码中,一旦插入值 Rust 就可以推断出想要存放的类型,所以你很少会需要这些类型注解。更常见的做法是使用初始值来创建一个`Vec`,而且为了方便 Rust 提供了`vec!`宏。这个宏会根据我们提供的值来创建一个新的`Vec`。如下代码会新建一个拥有值`1`、`2`和`3`的`Vec<i32>`:
|
||||
在实际的代码中,一旦插入值 Rust 就可以推断出想要存放的类型,所以你很少会需要这些类型注解。更常见的做法是使用初始值来创建一个 `Vec`,而且为了方便 Rust 提供了 `vec!` 宏。这个宏会根据我们提供的值来创建一个新的 `Vec`。如下代码会新建一个拥有值 `1`、`2` 和 `3` 的 `Vec<i32>`:
|
||||
|
||||
```rust
|
||||
let v = vec![1, 2, 3];
|
||||
```
|
||||
|
||||
因为我们提供了`i32`类型的初始值,Rust 可以推断出`v`的类型是`Vec<i32>`,因此类型注解就不是必须的。接下来让我们看看如何修改一个 vector。
|
||||
因为我们提供了 `i32` 类型的初始值,Rust 可以推断出 `v` 的类型是 `Vec<i32>`,因此类型注解就不是必须的。接下来让我们看看如何修改一个 vector。
|
||||
|
||||
### 更新 vector
|
||||
|
||||
对于新建一个 vector 并向其增加元素,可以使用`push`方法:
|
||||
对于新建一个 vector 并向其增加元素,可以使用 `push` 方法:
|
||||
|
||||
```rust
|
||||
let mut v = Vec::new();
|
||||
@ -37,11 +37,11 @@ v.push(7);
|
||||
v.push(8);
|
||||
```
|
||||
|
||||
如第三章中讨论的任何变量一样,如果想要能够改变它的值,必须使用`mut`关键字使其可变。放入其中的所有值都是`i32`类型的,而且 Rust 也根据数据如此判断,所以不需要`Vec<i32>`注解。
|
||||
如第三章中讨论的任何变量一样,如果想要能够改变它的值,必须使用 `mut` 关键字使其可变。放入其中的所有值都是 `i32` 类型的,而且 Rust 也根据数据如此判断,所以不需要 `Vec<i32>` 注解。
|
||||
|
||||
### 丢弃 vector 时也会丢弃其所有元素
|
||||
|
||||
类似于任何其他的`struct`,vector 在其离开作用域时会被释放:
|
||||
类似于任何其他的 `struct`,vector 在其离开作用域时会被释放:
|
||||
|
||||
```rust
|
||||
{
|
||||
@ -58,7 +58,7 @@ v.push(8);
|
||||
|
||||
现在你知道如何创建、更新和销毁 vector 了,接下来的一步最好了解一下如何读取他们的内容。有两种方法引用 vector 中储存的值。为了更加清楚的说明这个例子,我们标注这些函数返回的值的类型。
|
||||
|
||||
这个例子展示了访问 vector 中一个值的两种方式,索引语法或者`get`方法:
|
||||
这个例子展示了访问 vector 中一个值的两种方式,索引语法或者 `get` 方法:
|
||||
|
||||
```rust
|
||||
let v = vec![1, 2, 3, 4, 5];
|
||||
@ -67,7 +67,7 @@ let third: &i32 = &v[2];
|
||||
let third: Option<&i32> = v.get(2);
|
||||
```
|
||||
|
||||
这里有一些需要注意的地方。首先,我们使用索引值`2`来获取第三个元素,索引是从 0 开始的。其次,这两个不同的获取第三个元素的方式分别为:使用`&`和`[]`返回一个引用;或者使用`get`方法以索引作为参数来返回一个`Option<&T>`。
|
||||
这里有一些需要注意的地方。首先,我们使用索引值 `2` 来获取第三个元素,索引是从 0 开始的。其次,这两个不同的获取第三个元素的方式分别为:使用 `&` 和 `[]` 返回一个引用;或者使用 `get` 方法以索引作为参数来返回一个 `Option<&T>`。
|
||||
|
||||
Rust 有两个引用元素的方法的原因是程序可以选择如何处理当索引值在 vector 中没有对应值的情况。例如如下情况,如果有一个有五个元素的 vector 接着尝试访问索引为 100 的元素,程序该如何处理:
|
||||
|
||||
@ -78,9 +78,9 @@ let does_not_exist = &v[100];
|
||||
let does_not_exist = v.get(100);
|
||||
```
|
||||
|
||||
当运行这段代码,你会发现对于第一个`[]`方法,当引用一个不存在的元素时 Rust 会造成`panic!`。这个方法更适合当程序认为尝试访问超过 vector 结尾的元素是一个严重错误的情况,这时应该使程序崩溃。
|
||||
当运行这段代码,你会发现对于第一个 `[]` 方法,当引用一个不存在的元素时 Rust 会造成 `panic!`。这个方法更适合当程序认为尝试访问超过 vector 结尾的元素是一个严重错误的情况,这时应该使程序崩溃。
|
||||
|
||||
当`get`方法被传递了一个数组外的索引时,它不会 panic 而是返回`None`。当偶尔出现超过 vector 范围的访问属于正常情况的时候可以考虑使用它。接着你的代码可以有处理`Some(&element)`或`None`的逻辑,如第六章讨论的那样。例如,索引可能来源于用户输入的数字。如果他们不慎输入了一个过大的数字那么程序就会得到`None`值,你可以告诉用户`Vec`当前元素的数量并再请求他们输入一个有效的值。这就比因为输入错误而使程序崩溃要友好的多!
|
||||
当 `get` 方法被传递了一个数组外的索引时,它不会 panic 而是返回 `None`。当偶尔出现超过 vector 范围的访问属于正常情况的时候可以考虑使用它。接着你的代码可以有处理 `Some(&element)` 或 `None` 的逻辑,如第六章讨论的那样。例如,索引可能来源于用户输入的数字。如果他们不慎输入了一个过大的数字那么程序就会得到 `None` 值,你可以告诉用户 `Vec` 当前元素的数量并再请求他们输入一个有效的值。这就比因为输入错误而使程序崩溃要友好的多!
|
||||
|
||||
#### 无效引用
|
||||
|
||||
@ -96,7 +96,7 @@ v.push(6);
|
||||
|
||||
编译会给出这个错误:
|
||||
|
||||
```
|
||||
```text
|
||||
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as
|
||||
immutable
|
||||
|
|
||||
@ -133,11 +133,10 @@ let row = vec![
|
||||
];
|
||||
```
|
||||
|
||||
<span class="caption">Listing 8-1: Defining an enum to be able to hold
|
||||
different types of data in a vector</span>
|
||||
<span class="caption">列表 8-1:定义一个枚举,以便能在 vector 中存放不同类型的数据</span>
|
||||
|
||||
Rust 在编译时就必须准确的知道 vector 中类型的原因是它需要知道储存每个元素到底需要多少内存。第二个好处是可以准确的知道这个 vector 中允许什么类型。如果 Rust 允许 vector 存放任意类型,那么当对 vector 元素执行操作时一个或多个类型的值就有可能会造成错误。使用枚举外加`match`意味着 Rust 能在编译时就保证总是会处理所有可能的情况,正如第六章讲到的那样。
|
||||
Rust 在编译时就必须准确的知道 vector 中类型的原因在于它需要知道储存每个元素到底需要多少内存。第二个好处是可以准确的知道这个 vector 中允许什么类型。如果 Rust 允许 vector 存放任意类型,那么当对 vector 元素执行操作时一个或多个类型的值就有可能会造成错误。使用枚举外加 `match` 意味着 Rust 能在编译时就保证总是会处理所有可能的情况,正如第六章讲到的那样。
|
||||
|
||||
如果在编写程序时不能确切无遗的知道运行时会储存进 vector 的所有类型,枚举技术就行不通了。相反,你可以使用 trait 对象,第十七章会讲到它。
|
||||
|
||||
现在我们了解了一些使用 vector 的最常见的方式,请一定去看看标准库中`Vec`定义的很多其他实用方法的 API 文档。例如,除了`push`之外还有一个`pop`方法,它会移除并返回 vector 的最后一个元素。让我们继续下一个集合类型:`String`!
|
||||
现在我们了解了一些使用 vector 的最常见的方式,请一定去看看标准库中 `Vec` 定义的很多其他实用方法的 API 文档。例如,除了 `push` 之外还有一个 `pop` 方法,它会移除并返回 vector 的最后一个元素。让我们继续下一个集合类型:`String`!
|
@ -2,31 +2,31 @@
|
||||
|
||||
> [ch08-02-strings.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch08-02-strings.md)
|
||||
> <br>
|
||||
> commit d362dadae60a7cc3212b107b9e9562769b0f20e3
|
||||
> commit 692c4a78aac93670bc6f1fa5d33f71ed161b9339
|
||||
|
||||
第四章已经讲过一些字符串的内容,不过现在让我们更深入地了解一下它。字符串是新晋 Rustacean 们通常会被困住的领域。这是由于三方面内容的结合:Rust 倾向于确保暴露出可能的错误,字符串是比很多程序员所想象的要更为复杂的数据结构,以及 UTF-8。所有这些结合起来对于来自其他语言背景的程序员就可能显得很困难了。
|
||||
第四章已经讲过一些字符串的内容,不过现在让我们更深入地了解它。字符串是新晋 Rustacean 们通常会被困住的领域。这是由于三方面内容的结合:Rust 倾向于确保暴露出可能的错误,字符串是比很多程序员所想象的要更为复杂的数据结构,以及 UTF-8。所有这些结合起来对于来自其他语言背景的程序员就可能显得很困难了。
|
||||
|
||||
字符串出现在集合章节的原因是,字符串是作为字节的集合外加一些方法实现的,当这些字节被解释为文本时,这些方法提供了实用的功能。在这一部分,我们会讲到`String`那些任何集合类型都有的操作,比如创建、更新和读取。也会讨论`String`于其他集合不一样的地方,例如索引`String`是很复杂的,由于人和计算机理解`String`数据的不同方式。
|
||||
字符串出现在集合章节的原因是,字符串是作为字节的集合外加一些方法实现的,当这些字节被解释为文本时,这些方法提供了实用的功能。在这一部分,我们会讲到 `String` 中那些任何集合类型都有的操作,比如创建、更新和读取。也会讨论 `String` 与其他集合不一样的地方,例如索引` String` 是很复杂的,由于人和计算机理解 `String` 数据方式的不同。
|
||||
|
||||
### 什么是字符串?
|
||||
|
||||
在开始深入这些方面之前,我们需要讨论一下术语**字符串**的具体意义。Rust 的核心语言中事实上就只有一种字符串类型:`str`,字符串 slice,它通常以被借用的形式出现,`&str`。第四章讲到了**字符串 slice**:他们是一些储存在别处的 UTF-8 编码字符串数据的引用。比如字符串字面值被储存在程序的二进制输出中,字符串 slice 也是如此。
|
||||
在开始深入这些方面之前,我们需要讨论一下术语 **字符串** 的具体意义。Rust 的核心语言中事实上就只有一种字符串类型:`str`,字符串 slice,它通常以被借用的形式出现,`&str`。第四章讲到了 **字符串 slice**:他们是一些储存在别处的 UTF-8 编码字符串数据的引用。比如字符串字面值被储存在程序的二进制输出中,字符串 slice 也是如此。
|
||||
|
||||
称作`String`的类型是由标准库提供的,而没有写进核心语言部分,它是可增长的、可变的、有所有权的、UTF-8 编码的字符串类型。当 Rustacean 们谈到 Rust 的“字符串”时,他们通常指的是`String`和字符串 slice `&str`类型,而不是其中一个。这一部分大部分是关于`String`的,不过这些类型在 Rust 标准库中都被广泛使用。`String`和字符串 slice 都是 UTF-8 编码的。
|
||||
称作 `String` 的类型是由标准库提供的,而没有写进核心语言部分,它是可增长的、可变的、有所有权的、UTF-8 编码的字符串类型。当 Rustacean 们谈到 Rust 的 “字符串”时,他们通常指的是 `String` 和字符串 slice `&str`类型,而不是其中一个。这一部分大部分是关于 `String` 的,不过这些类型在 Rust 标准库中都被广泛使用。`String` 和字符串 slice 都是 UTF-8 编码的。
|
||||
|
||||
Rust 标准库中还包含一系列其他字符串类型,比如`OsString`、`OsStr`、`CString`和`CStr`。相关库 crate 甚至会提供更多储存字符串数据的选择。与`*String`/`*Str`的命名类似,他们通常也提供有所有权和可借用的变体,就比如说`String`/`&str`。这些字符串类型在储存的编码或内存表现形式上可能有所不同。本章将不会讨论其他这些字符串类型;查看 API 文档来更多的了解如何使用他们以及各自适合的场景。
|
||||
Rust 标准库中还包含一系列其他字符串类型,比如 `OsString`、`OsStr`、`CString` 和 `CStr`。相关库 crate 甚至会提供更多储存字符串数据的选择。与 `*String`/`*Str` 的命名类似,他们通常也提供有所有权和可借用的变体,就比如说 `String`/`&str`。这些字符串类型在储存的编码或内存表现形式上可能有所不同。本章将不会讨论其他这些字符串类型;查看 API 文档来更多的了解如何使用他们以及各自适合的场景。
|
||||
|
||||
### 新建字符串
|
||||
|
||||
很多`Vec`可用的操作在`String`中同样可用,从以`new`函数创建字符串开始,像这样:
|
||||
很多 `Vec` 可用的操作在 `String` 中同样可用,从以 `new` 函数创建字符串开始,像这样:
|
||||
|
||||
```rust
|
||||
let s = String::new();
|
||||
let mut s = String::new();
|
||||
```
|
||||
|
||||
这新建了一个叫做`s`的空的字符串,接着我们可以向其中装载数据。
|
||||
这新建了一个叫做 `s` 的空的字符串,接着我们可以向其中装载数据。
|
||||
|
||||
通常字符串会有初始数据因为我们希望一开始就有这个字符串。为此,使用`to_string`方法,它能用于任何实现了`Display` trait 的类型,对于字符串字面值是这样:
|
||||
通常字符串会有初始数据因为我们希望一开始就有这个字符串。为此,使用 `to_string` 方法,它能用于任何实现了 `Display` trait 的类型,对于字符串字面值是这样:
|
||||
|
||||
```rust
|
||||
let data = "initial contents";
|
||||
@ -37,15 +37,15 @@ let s = data.to_string();
|
||||
let s = "initial contents".to_string();
|
||||
```
|
||||
|
||||
这会创建一个包好`initial contents`的字符串。
|
||||
这会创建一个包含 `initial contents` 的字符串。
|
||||
|
||||
也可以使用`String::from`函数来从字符串字面值创建`String`。如下等同于使用`to_string`:
|
||||
也可以使用 `String::from` 函数来从字符串字面值创建 `String`。如下等同于使用 `to_string`:
|
||||
|
||||
```rust
|
||||
let s = String::from("initial contents");
|
||||
```
|
||||
|
||||
因为字符串使用广泛,这里有很多不同的用于字符串的通用 API 可供选择。他们有些可能显得有些多余,不过都有其用武之地!在这个例子中,`String::from`和`.to_string`最终做了完全相同的工作,所以如何选择就是风格问题了。
|
||||
因为字符串使用广泛,这里有很多不同的用于字符串的通用 API 可供选择。他们有些可能显得有些多余,不过都有其用武之地!在这个例子中,`String::from` 和 `.to_string` 最终做了完全相同的工作,所以如何选择就是风格问题了。
|
||||
|
||||
记住字符串是 UTF-8 编码的,所以可以包含任何可以正确编码的数据:
|
||||
|
||||
@ -65,18 +65,18 @@ let hello = "Hola";
|
||||
|
||||
### 更新字符串
|
||||
|
||||
`String`的大小可以增长其内容也可以改变,就像可以放入更多数据来改变`Vec`的内容一样。另外,`String`实现了`+`运算符作为级联运算符以便于使用。
|
||||
`String` 的大小可以增长其内容也可以改变,就像可以放入更多数据来改变 `Vec` 的内容一样。另外,`String` 实现了 `+` 运算符作为级联运算符以便于使用。
|
||||
|
||||
#### 使用 push 附加字符串
|
||||
|
||||
可以通过`push_str`方法来附加字符串 slice,从而使`String`变长:
|
||||
可以通过 `push_str` 方法来附加字符串 slice,从而使 `String` 变长:
|
||||
|
||||
```rust
|
||||
let mut s = String::from("foo");
|
||||
s.push_str("bar");
|
||||
```
|
||||
|
||||
执行这两行代码之后`s`将会包含“foobar”。`push_str`方法获取字符串 slice,因为并不需要获取参数的所有权。例如,如果将`s2`的内容附加到`s1`中后自身不能被使用就糟糕了:
|
||||
执行这两行代码之后 `s` 将会包含 “foobar”。`push_str` 方法获取字符串 slice,因为并不需要获取参数的所有权。例如,如果将 `s2` 的内容附加到 `s1` 中后自身不能被使用就糟糕了:
|
||||
|
||||
```rust
|
||||
let mut s1 = String::from("foo");
|
||||
@ -84,18 +84,18 @@ let s2 = String::from("bar");
|
||||
s1.push_str(&s2);
|
||||
```
|
||||
|
||||
`push`方法被定义为获取一个单独的字符作为参数,并附加到`String`中:
|
||||
`push` 方法被定义为获取一个单独的字符作为参数,并附加到 `String` 中:
|
||||
|
||||
```rust
|
||||
let mut s = String::from("lo");
|
||||
s.push('l');
|
||||
```
|
||||
|
||||
执行这些代码之后,`s`将会包含“lol”。
|
||||
执行这些代码之后,`s` 将会包含 “lol”。
|
||||
|
||||
#### 使用 + 运算符或`format!`宏级联字符串
|
||||
#### 使用 + 运算符或 `format!` 宏级联字符串
|
||||
|
||||
通常我们希望将两个已知的字符串合并在一起。一种办法是像这样使用`+`运算符:
|
||||
通常我们希望将两个已知的字符串合并在一起。一种办法是像这样使用 `+` 运算符:
|
||||
|
||||
```rust
|
||||
let s1 = String::from("Hello, ");
|
||||
@ -103,19 +103,19 @@ let s2 = String::from("world!");
|
||||
let s3 = s1 + &s2; // Note that s1 has been moved here and can no longer be used
|
||||
```
|
||||
|
||||
执行完这些代码之后字符串`s3`将会包含`Hello, world!`。`s1`在相加后不再有效的原因,和使用`s2`的引用的原因与使用`+`运算符时调用的方法签名有关,这个函数签名看起来像这样:
|
||||
执行完这些代码之后字符串 `s3` 将会包含 `Hello, world!`。`s1` 在相加后不再有效的原因,和使用 `s2` 的引用的原因与使用 `+` 运算符时调用的方法签名有关,这个函数签名看起来像这样:
|
||||
|
||||
```rust,ignore
|
||||
fn add(self, s: &str) -> String {
|
||||
```
|
||||
|
||||
这并不是标准库中实际的签名;那个`add`使用泛型定义。这里的签名使用具体类型代替了泛型,这也正是当使用`String`值调用这个方法会发生的。第十章会讨论泛型。这个签名提供了理解`+`运算那奇怪的部分的线索。
|
||||
这并不是标准库中实际的签名;那个 `add` 使用泛型定义。这里我们看到的 `add` 的签名使用具体类型代替了泛型,这也正是当使用 `String` 值调用这个方法会发生的。第十章会讨论泛型。这个签名提供了理解 `+` 运算那奇怪的部分的线索。
|
||||
|
||||
首先,`s2`使用了`&`,意味着我们使用第二个字符串的**引用**与第一个字符串相加。这是因为`add`函数的`s`参数:只能将`&str`和`String`相加,不能将两个`String`值相加。不过等一下——正如`add`的第二个参数所指定的,`&s2`的类型是`&String`而不是`&str`。那么为什么代码还能编译呢?之所以能够在`add`调用中使用`&s2`是因为`&String`可以被**强转**(*coerced*)成 `&str`——当`add`函数被调用时,Rust 使用了一个被称为**解引用强制多态**(*deref coercion*)的技术,你可以将其理解为它把`&s2`变成了`&s2[..]`以供`add`函数使用。第十五章会更深入的讨论解引用强制多态。因为`add`没有获取参数的所有权,所以`s2`在这个操作后仍然是有效的`String`。
|
||||
首先,`s2` 使用了 `&`,意味着我们使用第二个字符串的 **引用** 与第一个字符串相加。这是因为 `add` 函数的 `s` 参数:只能将 `&str` 和 `String` 相加,不能将两个 `String` 值相加。不过等一下——正如 `add` 的第二个参数所指定的,`&s2` 的类型是 `&String` 而不是 `&str`。那么为什么代码还能编译呢?之所以能够在 `add` 调用中使用 `&s2` 是因为 `&String` 可以被**强转**(*coerced*)成 `&str`——当`add`函数被调用时,Rust 使用了一个被称为 **解引用强制多态**(*deref coercion*)的技术,你可以将其理解为它把 `&s2` 变成了 `&s2[..]` 以供 `add` 函数使用。第十五章会更深入的讨论解引用强制多态。因为 `add` 没有获取参数的所有权,所以 `s2` 在这个操作后仍然是有效的 `String`。
|
||||
|
||||
其次,可以发现签名中`add`获取了`self`的所有权,因为`self`**没有**使用`&`。这意味着上面例子中的`s1`的所有权将被移动到`add`调用中,之后就不再有效。所以虽然`let s3 = s1 + &s2;`看起来就像它会复制两个字符串并创建一个新的字符串,而实际上这个语句会获取`s1`的所有权,附加上从`s2`中拷贝的内容,并返回结果的所有权。换句话说,它看起来好像生成了很多拷贝不过实际上并没有:这个实现比拷贝要更高效。
|
||||
其次,可以发现签名中 `add` 获取了 `self` 的所有权,因为 `self` **没有** 使用 `&`。这意味着上面例子中的 `s1` 的所有权将被移动到 `add` 调用中,之后就不再有效。所以虽然 `let s3 = s1 + &s2;` 看起来就像它会复制两个字符串并创建一个新的字符串,而实际上这个语句会获取 `s1` 的所有权,附加上从 `s2` 中拷贝的内容,并返回结果的所有权。换句话说,它看起来好像生成了很多拷贝不过实际上并没有:这个实现比拷贝要更高效。
|
||||
|
||||
如果想要级联多个字符串,`+`的行为就显得笨重了:
|
||||
如果想要级联多个字符串,`+` 的行为就显得笨重了:
|
||||
|
||||
```rust
|
||||
let s1 = String::from("tic");
|
||||
@ -125,7 +125,7 @@ let s3 = String::from("toe");
|
||||
let s = s1 + "-" + &s2 + "-" + &s3;
|
||||
```
|
||||
|
||||
这时`s`的内容会是“tic-tac-toe”。在有这么多`+`和`"`字符的情况下,很难理解具体发生了什么。对于更为复杂的字符串链接,可以使用`format!`宏:
|
||||
这时 `s` 的内容会是 “tic-tac-toe”。在有这么多 `+` 和 `"` 字符的情况下,很难理解具体发生了什么。对于更为复杂的字符串链接,可以使用 `format!` 宏:
|
||||
|
||||
```rust
|
||||
let s1 = String::from("tic");
|
||||
@ -135,11 +135,11 @@ let s3 = String::from("toe");
|
||||
let s = format!("{}-{}-{}", s1, s2, s3);
|
||||
```
|
||||
|
||||
这些代码也会将`s`设置为“tic-tac-toe”。`format!`与`println!`的工作原理相同,不过不同于将输出打印到屏幕上,它返回一个带有结果的`String`。这个版本就好理解的多,并且不会获取任何参数的所有权。
|
||||
这些代码也会将 `s` 设置为 “tic-tac-toe”。`format!` 与 `println!` 的工作原理相同,不过不同于将输出打印到屏幕上,它返回一个带有结果的 `String`。这个版本就好理解的多,并且不会获取任何参数的所有权。
|
||||
|
||||
### 索引字符串
|
||||
|
||||
在很多语言中,通过索引来引用字符串中的单独字符是有效且常见的操作。然而在 Rust 中,如果我们尝试使用索引语法访问`String`的一部分,会出现一个错误。比如如下代码:
|
||||
在很多语言中,通过索引来引用字符串中的单独字符是有效且常见的操作。然而在 Rust 中,如果我们尝试使用索引语法访问 `String` 的一部分,会出现一个错误。比如如下代码:
|
||||
|
||||
```rust,ignore
|
||||
let s1 = String::from("hello");
|
||||
@ -148,7 +148,7 @@ let h = s1[0];
|
||||
|
||||
会导致如下错误:
|
||||
|
||||
```
|
||||
```text
|
||||
error: the trait bound `std::string::String: std::ops::Index<_>` is not
|
||||
satisfied [--explain E0277]
|
||||
|>
|
||||
@ -161,13 +161,13 @@ note: the type `std::string::String` cannot be indexed by `_`
|
||||
|
||||
#### 内部表示
|
||||
|
||||
`String`是一个`Vec<u8>`的封装。让我们看看之前一些正确编码的字符串的例子。首先是这一个:
|
||||
`String` 是一个 `Vec<u8>` 的封装。让我们看看之前一些正确编码的字符串的例子。首先是这一个:
|
||||
|
||||
```rust
|
||||
let len = String::from("Hola").len();
|
||||
```
|
||||
|
||||
在这里,`len`的值是四,这意味着储存字符串“Hola”的`Vec`的长度是四个字节:每一个字符的 UTF-8 编码都占用一个字节。那下面这个例子又如何呢?
|
||||
在这里,`len` 的值是四,这意味着储存字符串 “Hola” 的 `Vec` 的长度是四个字节:每一个字符的 UTF-8 编码都占用一个字节。那下面这个例子又如何呢?
|
||||
|
||||
```rust
|
||||
let len = String::from("Здравствуйте").len();
|
||||
@ -182,38 +182,38 @@ let hello = "Здравствуйте";
|
||||
let answer = &hello[0];
|
||||
```
|
||||
|
||||
`answer`的值应该是什么呢?它应该是第一个字符`З`吗?当使用 UTF-8 编码时,`З`的第一个字节是`208`,第二个是`151`,所以`answer`实际上应该是`208`,不过`208`自身并不是一个有效的字母。返回`208`可不是一个请求字符串第一个字母的人所希望看到的,不过它是 Rust 在字节索引零位置所能提供的唯一数据。返回字节值可能不是人们希望看到的,即便是只有拉丁字母时:`&"hello"[0]`会返回`104`而不是`h`。为了避免返回意想不到值并造成不能立刻发现的 bug。Rust 选择不编译这些代码并及早杜绝了误会的发生。
|
||||
`answer` 的值应该是什么呢?它应该是第一个字符 `З` 吗?当使用 UTF-8 编码时,`З` 的第一个字节 `208`,第二个是 `151`,所以 `answer` 实际上应该是 `208`,不过 `208` 自身并不是一个有效的字母。返回 `208` 可不是一个请求字符串第一个字母的人所希望看到的,不过它是 Rust 在字节索引零位置所能提供的唯一数据。返回字节值可能不是人们希望看到的,即便是只有拉丁字母时:`&"hello"[0]` 会返回 `104` 而不是 `h`。为了避免返回意想不到值并造成不能立刻发现的 bug。Rust 选择不编译这些代码并及早杜绝了误会的发生。
|
||||
|
||||
#### 字节、标量值和字形簇!天呐!
|
||||
|
||||
这引起了关于 UTF-8 的另外一个问题:从 Rust 的角度来讲,事实上有三种相关方式可以理解字符串:字节、标量值和字形簇(最接近人们眼中**字母**的概念)。
|
||||
这引起了关于 UTF-8 的另外一个问题:从 Rust 的角度来讲,事实上有三种相关方式可以理解字符串:字节、标量值和字形簇(最接近人们眼中 **字母** 的概念)。
|
||||
|
||||
比如这个用梵文书写的印度语单词“नमस्ते”,最终它储存在`Vec`中的`u8`值看起来像这样:
|
||||
比如这个用梵文书写的印度语单词 “नमस्ते”,最终它储存在 `Vec` 中的 `u8` 值看起来像这样:
|
||||
|
||||
```
|
||||
```text
|
||||
[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164,
|
||||
224, 165, 135]
|
||||
```
|
||||
|
||||
这里有 18 个字节,也就是计算机最终会储存的数据。如果从 Unicode 标量值的角度理解他们,也就像 Rust 的`char`类型那样,这些字节看起来像这样:
|
||||
这里有 18 个字节,也就是计算机最终会储存的数据。如果从 Unicode 标量值的角度理解他们,也就像 Rust 的 `char` 类型那样,这些字节看起来像这样:
|
||||
|
||||
```
|
||||
```text
|
||||
['न', 'म', 'स', '्', 'त', 'े']
|
||||
```
|
||||
|
||||
这里有六个`char`,不过第四个和第六个都不是字母,他们是发音符号本身并没有任何意义。最后,如果以字形簇的角度理解,就会得到人们所说的构成这个单词的四个字母:
|
||||
这里有六个 `char`,不过第四个和第六个都不是字母,他们是发音符号本身并没有任何意义。最后,如果以字形簇的角度理解,就会得到人们所说的构成这个单词的四个字母:
|
||||
|
||||
```
|
||||
```text
|
||||
["न", "म", "स्", "ते"]
|
||||
```
|
||||
|
||||
Rust 提供了多种不同的方式来解释计算机储存的原始字符串数据,这样程序就可以选择它需要的表现方式,而无所谓是何种人类语言。
|
||||
|
||||
最后一个 Rust 不允许使用索引获取`String`字符的原因是索引操作预期总是需要常数时间 (O(1))。但是对于`String`不可能保证这样的性能,因为 Rust 不得不检查从字符串的开头到索引位置的内容来确定这里有多少有效的字符。
|
||||
最后一个 Rust 不允许使用索引获取 `String` 字符的原因是索引操作预期总是需要常数时间 (O(1))。但是对于 `String` 不可能保证这样的性能,因为 Rust 不得不检查从字符串的开头到索引位置的内容来确定这里有多少有效的字符。
|
||||
|
||||
### 字符串 slice
|
||||
|
||||
因为字符串索引应该返回的类型是不明确的,而且索引字符串通常也是一个坏点子,所以 Rust 不建议这么做,而如果你确实需要它的话则需要更加明确一些。比使用`[]`和单个值的索引更加明确的方式是使用`[]`和一个 range 来创建包含特定字节的字符串 slice:
|
||||
因为字符串索引应该返回的类型是不明确的,而且索引字符串通常也是一个坏点子,所以 Rust 不建议这么做,而如果你确实需要它的话则需要更加明确一些。比使用 `[]` 和单个值的索引更加明确的方式是使用 `[]` 和一个 range 来创建包含特定字节的字符串 slice:
|
||||
|
||||
```rust
|
||||
let hello = "Здравствуйте";
|
||||
@ -221,10 +221,9 @@ let hello = "Здравствуйте";
|
||||
let s = &hello[0..4];
|
||||
```
|
||||
|
||||
这里,`s`是一个`&str`,它包含字符串的头四个字节。早些时候,我们提到了这些字母都是两个字节长的,所以这意味着`s`将会是“Зд”。
|
||||
|
||||
如果获取`&hello[0..1]`会发生什么呢?答案是:在运行时会 panic,就跟访问 vector 中的无效索引时一样:
|
||||
这里,`s` 是一个 `&str`,它包含字符串的头四个字节。早些时候,我们提到了这些字母都是两个字节长的,所以这意味着 `s` 将会是 “Зд”。
|
||||
|
||||
如果获取 `&hello[0..1]` 会发生什么呢?答案是:在运行时会 panic,就跟访问 vector 中的无效索引时一样:
|
||||
|
||||
```text
|
||||
thread 'main' panicked at 'index 0 and/or 1 in `Здравствуйте` do not lie on
|
||||
@ -237,7 +236,7 @@ character boundary', ../src/libcore/str/mod.rs:1694
|
||||
|
||||
幸运的是,这里还有其他获取字符串元素的方式。
|
||||
|
||||
如果你需要操作单独的 Unicode 标量值,最好的选择是使用`chars`方法。对“नमस्ते”调用`chars`方法会将其分开并返回六个`char`类型的值,接着就可以遍历结果来访问每一个元素了:
|
||||
如果你需要操作单独的 Unicode 标量值,最好的选择是使用 `chars` 方法。对 “नमस्ते” 调用 `chars` 方法会将其分开并返回六个 `char` 类型的值,接着就可以遍历结果来访问每一个元素了:
|
||||
|
||||
```rust
|
||||
for c in "नमस्ते".chars() {
|
||||
@ -247,7 +246,7 @@ for c in "नमस्ते".chars() {
|
||||
|
||||
这些代码会打印出如下内容:
|
||||
|
||||
```
|
||||
```text
|
||||
न
|
||||
म
|
||||
स
|
||||
@ -256,8 +255,7 @@ for c in "नमस्ते".chars() {
|
||||
े
|
||||
```
|
||||
|
||||
`bytes`方法返回每一个原始字节,这可能会适合你的使用场景:
|
||||
|
||||
`bytes` 方法返回每一个原始字节,这可能会适合你的使用场景:
|
||||
|
||||
```rust
|
||||
for b in "नमस्ते".bytes() {
|
||||
@ -265,9 +263,9 @@ for b in "नमस्ते".bytes() {
|
||||
}
|
||||
```
|
||||
|
||||
这些代码会打印出组成`String`的 18 个字节,开头是这样的:
|
||||
这些代码会打印出组成 `String` 的 18 个字节,开头是这样的:
|
||||
|
||||
```
|
||||
```text
|
||||
224
|
||||
164
|
||||
168
|
||||
@ -281,6 +279,6 @@ for b in "नमस्ते".bytes() {
|
||||
|
||||
### 字符串并不简单
|
||||
|
||||
总而言之,字符串还是很复杂的。不同的语言选择了不同的向程序员展示其复杂性的方式。Rust 选择了以准确的方式处理`String`数据作为所有 Rust 程序的默认行为,这意味着程序员们必须更多的思考如何在前台处理 UTF-8 数据。这种权衡取舍相比其他语言更多的暴露出了字符串的复杂性,不过也使你在开发生命周期中免于处理涉及非 ASCII 字符的错误。
|
||||
总而言之,字符串还是很复杂的。不同的语言选择了不同的向程序员展示其复杂性的方式。Rust 选择了以准确的方式处理 `String` 数据作为所有 Rust 程序的默认行为,这意味着程序员们必须更多的思考如何在前台处理 UTF-8 数据。这种权衡取舍相比其他语言更多的暴露出了字符串的复杂性,不过也使你在开发生命周期中免于处理涉及非 ASCII 字符的错误。
|
||||
|
||||
现在让我们转向一些不太复杂的集合:哈希 map!
|
||||
|
@ -2,17 +2,17 @@
|
||||
|
||||
> [ch08-03-hash-maps.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch08-03-hash-maps.md)
|
||||
> <br>
|
||||
> commit 4f2dc564851dc04b271a2260c834643dfd86c724
|
||||
> commit d06a6a181fd61704cbf7feb55bc61d518c6469f9
|
||||
|
||||
最后介绍的常用集合类型是 **哈希 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 中,其中键是队伍的名字而值是每个队伍的分数。给出一个队名,就能得到他们的得分。
|
||||
|
||||
本章我们会介绍哈希 map 的基本 API,不过还有更多吸引人的功能隐藏于标准库中的`HashMap`定义的函数中。请一如既往地查看标准库文档来了解更多信息。
|
||||
本章我们会介绍哈希 map 的基本 API,不过还有更多吸引人的功能隐藏于标准库中 `HashMap` 定义的函数中。请一如既往地查看标准库文档来了解更多信息。
|
||||
|
||||
### 新建一个哈希 map
|
||||
|
||||
可以使用`new`创建一个空的`HashMap`,并使用`insert`来增加元素。这里我们记录两支队伍的分数,分别是蓝队和黄队。蓝队开始有 10 分而黄队开始有 50 分:
|
||||
可以使用 `new` 创建一个空的 `HashMap`,并使用 `insert` 增加元素。这里我们记录两支队伍的分数,分别是蓝队和黄队。蓝队开始有 10 分而黄队开始有 50 分:
|
||||
|
||||
```rust
|
||||
use std::collections::HashMap;
|
||||
@ -26,7 +26,7 @@ scores.insert(String::from("Yellow"), 50);
|
||||
注意必须首先 `use` 标准库中集合部分的 `HashMap`。在这三个常用集合中,`HashMap` 是最不常用的,所以并没有被 prelude 自动引用。标准库中对 `HashMap` 的支持也相对较少,例如,并没有内建的构建宏。
|
||||
像 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;
|
||||
@ -37,11 +37,11 @@ let initial_scores = vec![10, 50];
|
||||
let scores: HashMap<_, _> = teams.iter().zip(initial_scores.iter()).collect();
|
||||
```
|
||||
|
||||
这里`HashMap<_, _>`类型注解是必要的,因为可能`collect`进很多不同的数据结构,而除非显式指定 Rust 无从得知你需要的类型。但是对于键和值的类型参数来说,可以使用下划线占位,而 Rust 能够根据 vector 中数据的类型推断出 `HashMap` 所包含的类型。
|
||||
这里 `HashMap<_, _>` 类型注解是必要的,因为可能 `collect` 很多不同的数据结构,而除非显式指定否则 Rust 无从得知你需要的类型。但是对于键和值的类型参数来说,可以使用下划线占位,而 Rust 能够根据 vector 中数据的类型推断出 `HashMap` 所包含的类型。
|
||||
|
||||
### 哈希 map 和所有权
|
||||
|
||||
对于像`i32`这样的实现了`Copy` trait 的类型,其值可以拷贝进哈希 map。对于像`String`这样拥有所有权的值,其值将被移动而哈希 map 会成为这些值的所有者:
|
||||
对于像 `i32` 这样的实现了 `Copy` trait 的类型,其值可以拷贝进哈希 map。对于像 `String` 这样拥有所有权的值,其值将被移动而哈希 map 会成为这些值的所有者:
|
||||
|
||||
```rust
|
||||
use std::collections::HashMap;
|
||||
@ -54,13 +54,13 @@ map.insert(field_name, field_value);
|
||||
// field_name and field_value are invalid at this point
|
||||
```
|
||||
|
||||
当`insert`调用将`field_name`和`field_value`移动到哈希 map 中后,将不能使用这两个绑定。
|
||||
当 `insert` 调用将 `field_name` 和 `field_value` 移动到哈希 map 中后,将不能使用这两个绑定。
|
||||
|
||||
如果将值的引用插入哈希 map,这些值本身将不会被移动进哈希 map。但是这些引用指向的值必须至少在哈希 map 有效时也是有效的。第十章生命周期部分将会更多的讨论这个问题。
|
||||
|
||||
### 访问哈希 map 中的值
|
||||
|
||||
可以通过`get`方法并提供对应的键来从哈希 map 中获取值:
|
||||
可以通过 `get` 方法并提供对应的键来从哈希 map 中获取值:
|
||||
|
||||
```rust
|
||||
use std::collections::HashMap;
|
||||
@ -76,7 +76,7 @@ let score = scores.get(&team_name);
|
||||
|
||||
这里,`score` 是与蓝队分数相关的值,应为 `Some(10)`。因为 `get` 返回 `Option<V>`,所以结果被装进 `Some`;如果某个键在哈希 map 中没有对应的值,`get` 会返回 `None`。这时就要用某种第六章提到的方法来处理 `Option`。
|
||||
|
||||
可以使用与 vector 类似的方式来遍历哈希 map 中的每一个键值对,也就是`for`循环:
|
||||
可以使用与 vector 类似的方式来遍历哈希 map 中的每一个键值对,也就是 `for` 循环:
|
||||
|
||||
```rust
|
||||
use std::collections::HashMap;
|
||||
@ -93,17 +93,18 @@ for (key, value) in &scores {
|
||||
|
||||
这会以任意顺序打印出每一个键值对:
|
||||
|
||||
```
|
||||
```text
|
||||
Yellow: 50
|
||||
Blue: 10
|
||||
```
|
||||
|
||||
### 更新哈希 map
|
||||
|
||||
尽管键值对的数量是可以增长的,不过任何时候,每个键只能关联一个值。当你想要改变哈希 map 中的数据时,根据目标键是否有值以及值的更新策略分成多种情况,下面我们了解一下:
|
||||
尽管键值对的数量是可以增长的,不过任何时候,每个键只能关联一个值。当我们想要改变哈希 map 中的数据时,必须决定如何处理一个键已经有值了的情况。可以选择完全无视旧值并用新值代替旧值。可以选择保留旧值而忽略新值,并只在键 **没有** 对应值时增加新值。或者可以结合新旧两值。让我们看看这分别该如何处理!
|
||||
|
||||
#### 覆盖一个值
|
||||
|
||||
如果我们插入了一个键值对,接着用相同的键插入一个不同的值,与这个键相关联的旧值将被替换。即便下面的代码调用了两次`insert`,哈希 map 也只会包含一个键值对,因为两次都是对蓝队的键插入的值:
|
||||
如果我们插入了一个键值对,接着用相同的键插入一个不同的值,与这个键相关联的旧值将被替换。即便下面的代码调用了两次 `insert`,哈希 map 也只会包含一个键值对,因为两次都是对蓝队的键插入的值:
|
||||
|
||||
```rust
|
||||
use std::collections::HashMap;
|
||||
@ -116,11 +117,11 @@ scores.insert(String::from("Blue"), 25);
|
||||
println!("{:?}", scores);
|
||||
```
|
||||
|
||||
这会打印出`{"Blue": 25}`。原始的值 10 将被覆盖。
|
||||
这会打印出 `{"Blue": 25}`。原始的值 10 将被覆盖。
|
||||
|
||||
#### 只在键没有对应值时插入
|
||||
|
||||
我们经常会检查某个特定的键是否有值,如果没有就插入一个值。为此哈希 map 有一个特有的 API,叫做`entry`,它获取我们想要检查的键作为参数。`entry`函数的返回值是一个枚举,`Entry`,它代表了可能存在也可能不存在的值。比如说我们想要检查黄队的键是否关联了一个值。如果没有,就插入值 50,对于蓝队也是如此。使用 entry API 的代码看起来像这样:
|
||||
我们经常会检查某个特定的键是否有值,如果没有就插入一个值。为此哈希 map 有一个特有的 API,叫做 `entry`,它获取我们想要检查的键作为参数。`entry` 函数的返回值是一个枚举,`Entry`,它代表了可能存在也可能不存在的值。比如说我们想要检查黄队的键是否关联了一个值。如果没有,就插入值 50,对于蓝队也是如此。使用 entry API 的代码看起来像这样:
|
||||
|
||||
```rust
|
||||
use std::collections::HashMap;
|
||||
@ -134,13 +135,13 @@ scores.entry(String::from("Blue")).or_insert(50);
|
||||
println!("{:?}", scores);
|
||||
```
|
||||
|
||||
`Entry`的`or_insert`方法在键对应的值存在时就返回这个值的`Entry`,如果不存在则将参数作为新值插入并返回修改过的`Entry`。这比编写自己的逻辑要简明的多,另外也与借用检查器结合得更好。
|
||||
`Entry` 的 `or_insert` 方法在键对应的值存在时就返回这个值的 `Entry`,如果不存在则将参数作为新值插入并返回修改过的 `Entry`。这比编写自己的逻辑要简明的多,另外也与借用检查器结合得更好。
|
||||
|
||||
这段代码会打印出`{"Yellow": 50, "Blue": 10}`。第一个`entry`调用会插入黄队的键和值 50,因为黄队并没有一个值。第二个`entry`调用不会改变哈希 map 因为蓝队已经有了值 10。
|
||||
这段代码会打印出 `{"Yellow": 50, "Blue": 10}`。第一个 `entry` 调用会插入黄队的键和值 50,因为黄队并没有一个值。第二个 `entry` 调用不会改变哈希 map 因为蓝队已经有了值 10。
|
||||
|
||||
#### 根据旧值更新一个值
|
||||
|
||||
另一个常见的哈希 map 的应用场景是找到一个键对应的值并根据旧的值更新它。例如,如果我们想要计数一些文本中每一个单词分别出现了多少次,就可以使用哈希 map,以单词作为键并递增其值来记录我们遇到过几次这个单词。如果是第一次看到某个单词,就插入值`0`。
|
||||
另一个常见的哈希 map 的应用场景是找到一个键对应的值并根据旧的值更新它。例如,如果我们想要计数一些文本中每一个单词分别出现了多少次,就可以使用哈希 map,以单词作为键并递增其值来记录我们遇到过几次这个单词。如果是第一次看到某个单词,就插入值 `0`。
|
||||
|
||||
|
||||
```rust
|
||||
@ -158,19 +159,19 @@ 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 的库。
|
||||
|
||||
## 总结
|
||||
|
||||
vector、字符串和哈希 map 会在你的程序需要储存、访问和修改数据时帮助你。这里有一些你应该能够解决的练习问题:
|
||||
|
||||
* 给定一系列数字,使用 vector 并返回这个列表的平均数(mean, average)、中位数(排列数组后位于中间的值)和众数(mode,出现次数最多的值;这里哈希函数会很有帮助)。
|
||||
* 将字符串转换为 Pig Latin,也就是每一个单词的第一个辅音字母被移动到单词的结尾并增加“ay”,所以“first”会变成“irst-fay”。元音字母开头的单词则在结尾增加 “hay”(“apple”会变成“apple-hay”)。牢记 UTF-8 编码!
|
||||
* 使用哈希 map 和 vector,创建一个文本接口来允许用户向公司的部门中增加员工的名字。例如,“Add Sally to Engineering”或“Add Amir to Sales”。接着让用户获取一个部门的所有员工的列表,或者公司每个部门的所有员工按照字母顺排序的列表。
|
||||
* 将字符串转换为 Pig Latin,也就是每一个单词的第一个辅音字母被移动到单词的结尾并增加 “ay”,所以 “first” 会变成 “irst-fay”。元音字母开头的单词则在结尾增加 “hay”(“apple” 会变成 “apple-hay”)。牢记 UTF-8 编码!
|
||||
* 使用哈希 map 和 vector,创建一个文本接口来允许用户向公司的部门中增加员工的名字。例如,“Add Sally to Engineering” 或 “Add Amir to Sales”。接着让用户获取一个部门的所有员工的列表,或者公司每个部门的所有员工按照字母顺排序的列表。
|
||||
|
||||
标准库 API 文档中描述的这些类型的方法将有助于你进行这些练习!
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user