定义模块以控制作用域和隐私

在本节中,我们将讨论模块和模块系统的其他部分,即允许您命名条目的*路径*;将路径引入作用域的 use 关键字;以及使条目公开的 pub 关键字。我们还将讨论 as 关键字、外部包和通配符运算符。

首先,我们将从一系列规则开始,以便您将来在组织代码时可以轻松参考。然后,我们将详细解释每条规则。

模块速查表

在这里,我们提供了关于模块、路径、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 处找到。
  • 私有与公共:默认情况下,模块内的代码对其父模块是私有的。要使模块公开,请使用 pub mod 而不是 mod 声明它。要使公共模块中的条目也公开,请在其声明之前使用 pub
  • use 关键字:在作用域内,use 关键字为条目创建快捷方式,以减少长路径的重复。在任何可以引用 crate::garden::vegetables::Asparagus 的作用域中,您都可以使用 use crate::garden::vegetables::Asparagus; 创建快捷方式,从那时起,您只需要编写 Asparagus 即可在作用域中使用该类型。

这里我们创建一个名为 backyard 的二进制 crate 来演示这些规则。crate 的目录也名为 backyard,包含以下文件和目录

backyard
├── Cargo.lock
├── Cargo.toml
└── src
    ├── garden
    │   └── vegetables.rs
    ├── garden.rs
    └── main.rs

在这种情况下,crate 根文件是 *src/main.rs*,它包含

文件名:src/main.rs

use crate::garden::vegetables::Asparagus;

pub mod garden;

fn main() {
    let plant = Asparagus {};
    println!("I'm growing {plant:?}!");
}

pub mod garden; 行告诉编译器包含它在 *src/garden.rs* 中找到的代码,该代码为

文件名:src/garden.rs

pub mod vegetables;

这里,pub mod vegetables; 表示 *src/garden/vegetables.rs* 中的代码也包括在内。该代码为

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

现在让我们详细了解这些规则,并在实际操作中演示它们!

*模块* 允许我们在 crate 中组织代码,以提高可读性和易用性。模块还允许我们控制条目的*隐私*,因为模块内的代码默认是私有的。私有条目是内部实现细节,不可供外部使用。我们可以选择公开模块及其中的条目,这将公开它们以允许外部代码使用和依赖它们。

举个例子,让我们编写一个库 crate 来提供餐厅的功能。我们将定义函数的签名,但将其函数体留空,以便专注于代码的组织,而不是餐厅的实现。

在餐饮业中,餐厅的某些部分被称为“前厅”,而另一些部分被称为“后厨”。前厅是顾客所在的地方;这包括迎宾员安排顾客座位、服务员点菜和收款以及调酒师调制饮料的地方。后厨是厨师和帮厨在厨房工作、洗碗工清理以及经理进行行政工作的地方。

为了以这种方式构建我们的 crate,我们可以将它的函数组织到嵌套模块中。通过运行 `cargo new restaurant --lib` 创建一个名为 `restaurant` 的新库;然后将代码清单 7-1 中的代码输入到 *src/lib.rs* 中,以定义一些模块和函数签名。以下是前厅部分

文件名:src/lib.rs

mod front_of_house {
    mod hosting {
        fn add_to_waitlist() {}

        fn seat_at_table() {}
    }

    mod serving {
        fn take_order() {}

        fn serve_order() {}

        fn take_payment() {}
    }
}

代码清单 7-1:包含其他模块的 `front_of_house` 模块,这些模块又包含函数

我们使用 `mod` 关键字后跟模块名称(在本例中为 `front_of_house`)来定义模块。然后,模块的主体放在花括号内。在模块内部,我们可以放置其他模块,如本例中的 `hosting` 和 `serving` 模块。模块还可以包含其他项的定义,例如结构体、枚举、常量、trait 以及(如代码清单 7-1 中所示)函数。

通过使用模块,我们可以将相关的定义分组在一起,并说明它们之间的关联性。使用此代码的程序员可以根据组而不是必须阅读所有定义来浏览代码,从而更容易找到与他们相关的定义。向此代码添加新功能的程序员将知道将代码放置在何处以保持程序的有序性。

之前,我们提到过 *src/main.rs* 和 *src/lib.rs* 被称为 crate 根。之所以这样命名,是因为这两个文件中的任何一个的内容都构成了一个名为 `crate` 的模块,该模块位于 crate 模块结构的根部,称为_模块树_。

代码清单 7-2 显示了代码清单 7-1 中结构的模块树。

crate
 └── front_of_house
     ├── hosting
     │   ├── add_to_waitlist
     │   └── seat_at_table
     └── serving
         ├── take_order
         ├── serve_order
         └── take_payment

代码清单 7-2:代码清单 7-1 中代码的模块树

此树显示了一些模块如何嵌套在彼此内部;例如,`hosting` 嵌套在 `front_of_house` 内部。该树还显示了一些模块是彼此的_兄弟_,这意味着它们是在同一个模块中定义的;`hosting` 和 `serving` 是在 `front_of_house` 中定义的兄弟模块。如果模块 A 包含在模块 B 中,我们说模块 A 是模块 B 的_子模块_,而模块 B 是模块 A 的_父模块_。请注意,整个模块树都植根于名为 `crate` 的隐式模块下。

模块树可能会让您想起计算机上的文件系统目录树;这是一个非常恰当的比较!就像文件系统中的目录一样,您可以使用模块来组织代码。就像目录中的文件一样,我们需要一种方法来查找我们的模块。