使用模块控制作用域和隐私性
本节将讨论模块及模块系统的其他部分:用于为项目命名的路径、将路径引入作用域的 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找到。 - 私有与公开:默认情况下,模块中的代码对于其父模块是私有的。要将模块设为公开,应使用
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,其内容如下:
use crate::garden::vegetables::Asparagus;
pub mod garden;
fn main() {
let plant = Asparagus {};
println!("I'm growing {plant:?}!");
}
pub mod garden; 这一行告诉编译器包含 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,定义一些模块和函数签名;这些代码属于前厅部分。
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() {}
}
}
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
这棵树展示了模块如何嵌套在其他模块中;例如,hosting 嵌套在 front_of_house 中。树还展示了一些模块是同级模块,也就是说它们定义在同一个模块中;hosting 和 serving 就是定义在 front_of_house 中的同级模块。如果模块 A 位于模块 B 内部,我们就称模块 A 是模块 B 的子模块,模块 B 是模块 A 的父模块。请注意,整个模块树都以隐式模块 crate 为根。
模块树可能会让你想到计算机文件系统中的目录树;这是一个非常贴切的类比!就像用文件系统中的目录组织文件一样,我们使用模块组织代码。正如我们需要一种方式查找目录中的文件,也需要一种方式查找模块。