定义模块来控制作用域与私有性

ch07-02-defining-modules-to-control-scope-and-privacy.md
commit 310ea6cb0dd855eaf510c9ba05648bc5836ead0c

在本节,我们将讨论模块和其它一些关于模块系统的部分,如允许你命名项的 路径paths);用来将路径引入作用域的 use 关键字;以及使项变为公有的 pub 关键字。我们还将讨论 as 关键字、外部包和 glob 运算符。现在,让我们把注意力放在模块上!

首先,我们将从一系列的规则开始,在你未来组织代码的时候,这些规则可被用作简单的参考。接下来我们将会详细的解释每条规则。

模块小抄

这里我们提供一个简单的参考,用来解释模块、路径、use关键词和pub关键词如何在编译器中工作,和大部分开发者如何组织他们的代码。我们将在这个章节中对每条规则的例子一一列举,但这是一个用来参考的好地方用于表达模块是如何工作的。

  • 从crate根节点开始: 当编译一个crate, 编译器首先在crate根文件(通常,对于一个库crate而言是src/lib.rs,对于一个二进制crate而言是src/main.rs)中寻找需要被编译的代码。
  • 声明模块: 在crate根文件中,你可以声明一个新模块;比如,你用mod garden声明了一个叫做garden的模块。编译器会在下列路径中寻找模块代码:
    • 内联,在大括号中,当mod garden后方不是一个分号而是一个大括号
    • 在文件 src/garden.rs
    • 在文件 src/garden/mod.rs
  • 声明子模块: 在除了crate根节点以外的其他文件中,你可以定义子模块。比如,你可能在src/garden.rs中定义了mod vegetables;。编译器会在以父模块命名的目录中寻找子模块代码:
    • 内联, 在大括号中,当mod vegetables后方不是一个分号而是一个大括号
    • 在文件 src/garden/vegetables.rs
    • 在文件 src/garden/vegetables/mod.rs
  • 模块中的代码路径: 一旦一个模块是你crate的一部分, 你可以在隐私规则允许的前提下,从同一个crate内的任意地方,通过代码路径引用该模块的代码。举例而言,一个garden vegetables模块下的Asparagus类型可以在crate::garden::vegetables::Asparagus被找到。
  • 私有 vs 公用: 一个模块里的代码默认对其父模块私有。为了使一个模块公用,应当在声明时使用pub mod替代mod。为了使一个公用模块内部的成员公用,应当在声明前使用pub
  • use 关键字: 在一个作用域内,use关键字创建了一个成员的快捷方式,用来减少长路径的重复。在任何可以引用crate::garden::vegetables::Asparagus的作用域, 你可以通过 use crate::garden::vegetables::Asparagus;创建一个快捷方式,然后你就可以在作用域中只写Asparagus来使用该类型。

这里我们创建一个名为backyard的二进制crate来说明这些规则。该crate的路径同样命名为backyard,该路径包含了这些文件和目录:

  1. backyard
  2. ├── Cargo.lock
  3. ├── Cargo.toml
  4. └── src
  5. ├── garden
  6. └── vegetables.rs
  7. ├── garden.rs
  8. └── main.rs

这个例子中的crate根文件是src/main.rs,该文件包括了:

Filename: src/main.rs

  1. use crate::garden::vegetables::Asparagus;
  2. pub mod garden;
  3. fn main() {
  4. let plant = Asparagus {};
  5. println!("I'm growing {:?}!", plant);
  6. }

pub mod garden;行告诉编译器应该包含在src/garden.rs文件中发现的代码:

Filename: src/garden.rs

  1. pub mod vegetables;

在此处, pub mod vegetables;意味着在src/garden/vegetables.rs中的代码也应该被包括。这些代码是:

  1. #[derive(Debug)]
  2. pub struct Asparagus {}

现在让我们深入了解这些规则的细节并在实际中演示它们!

在模块中对相关代码进行分组

模块 让我们可以将一个 crate 中的代码进行分组,以提高可读性与重用性。模块还可以控制项的 私有性,即项是可以被外部代码使用的(public),还是作为一个内部实现的内容,不能被外部代码使用(private)。

在餐饮业,餐馆中会有一些地方被称之为 前台front of house),还有另外一些地方被称之为 后台back of house)。前台是招待顾客的地方,在这里,店主可以为顾客安排座位,服务员接受顾客下单和付款,调酒师会制作饮品。后台则是由厨师工作的厨房,洗碗工的工作地点,以及经理做行政工作的地方组成。

我们可以将函数放置到嵌套的模块中,来使我们的 crate 结构与实际的餐厅结构相同。通过执行 cargo new --lib restaurant,来创建一个新的名为 restaurant 的库。然后将示例 7-1 中所罗列出来的代码放入 src/lib.rs 中,来定义一些模块和函数。

文件名: src/lib.rs

  1. mod front_of_house {
  2. mod hosting {
  3. fn add_to_waitlist() {}
  4. fn seat_at_table() {}
  5. }
  6. mod serving {
  7. fn take_order() {}
  8. fn serve_order() {}
  9. fn take_payment() {}
  10. }
  11. }

示例 7-1:一个包含了其他内置了函数的模块的 front_of_house 模块

我们定义一个模块,是以 mod 关键字为起始,然后指定模块的名字(本例中叫做 front_of_house),并且用花括号包围模块的主体。在模块内,我们还可以定义其他的模块,就像本例中的 hostingserving 模块。模块还可以保存一些定义的其他项,比如结构体、枚举、常量、特性、或者函数。

通过使用模块,我们可以将相关的定义分组到一起,并指出他们为什么相关。程序员可以通过使用这段代码,更加容易地找到他们想要的定义,因为他们可以基于分组来对代码进行导航,而不需要阅读所有的定义。程序员向这段代码中添加一个新的功能时,他们也会知道代码应该放置在何处,可以保持程序的组织性。

在前面我们提到了,src/main.rssrc/lib.rs 叫做 crate 根。之所以这样叫它们是因为这两个文件的内容都分别在 crate 模块结构的根组成了一个名为 crate 的模块,该结构被称为 模块树module tree)。

示例 7-2 展示了示例 7-1 中的模块树的结构。

  1. crate
  2. └── front_of_house
  3. ├── hosting
  4. ├── add_to_waitlist
  5. └── seat_at_table
  6. └── serving
  7. ├── take_order
  8. ├── serve_order
  9. └── take_payment

示例 7-2: 示例 7-1 中代码的模块树

这个树展示了一些模块是如何被嵌入到另一个模块的(例如,hosting 嵌套在 front_of_house 中)。这个树还展示了一些模块是互为 兄弟siblings) 的,这意味着它们定义在同一模块中(hostingserving 被一起定义在 front_of_house 中)。继续沿用家庭关系的比喻,如果一个模块 A 被包含在模块 B 中,我们将模块 A 称为模块 B 的 child),模块 B 则是模块 A 的 parent)。注意,整个模块树都植根于名为 crate 的隐式模块下。

这个模块树可能会令你想起电脑上文件系统的目录树;这是一个非常恰当的类比!就像文件系统的目录,你可以使用模块来组织你的代码。并且,就像目录中的文件,我们需要一种方法来找到模块。