定义模块来控制作用域和隐私
在本节中,我们将讨论模块和模块系统的其他部分,即 *路径*,它允许你命名项;`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`,包含以下文件和目录
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` 的隐式模块下。
模块树可能会让你想起计算机上文件系统的目录树;这是一个非常恰当的比较!就像文件系统中的目录一样,你使用模块来组织你的代码。就像目录中的文件一样,我们需要一种方法来找到我们的模块。