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

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

模块速查表

在我们深入模块和路径的细节之前,这里提供了一个快速参考,介绍了模块、路径、use 关键字和 pub 关键字在编译器中的工作原理,以及大多数开发人员如何组织他们的代码。我们将在本章中通过每个规则的示例进行讲解,但这是一个很好的地方,可以作为模块工作方式的提醒。

  • 从crate根开始: 在编译crate时,编译器首先 在crate根文件(通常为库crate的src/lib.rs或 二进制crate的src/main.rs)中查找要编译的代码。
  • 声明模块:在crate根文件中,你可以声明新的模块;比如说你声明了一个“garden”模块,使用mod 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,它包含:

Filename: 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 中找到的代码,即:

Filename: 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 中,以定义一些模块和函数签名;这段代码是前厅部分。

Filename: 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() {}
    }
}
Listing 7-1: A front_of_house module containing other modules that then contain functions

我们使用 mod 关键字后跟模块的名称(在本例中为 front_of_house)来定义一个模块。模块的主体内容放在大括号内。在模块内部,我们可以放置其他模块,如本例中的 hostingserving 模块。模块还可以包含其他项目的定义,例如结构体、枚举、常量、特征以及如清单 7-1 中的函数。

通过使用模块,我们可以将相关的定义组合在一起,并命名它们为什么相关。使用此代码的程序员可以根据这些组来导航代码,而不需要阅读所有的定义,这使得更容易找到与他们相关的定义。向此代码添加新功能的程序员会知道将代码放置在哪里以保持程序的组织性。

早些时候,我们提到 src/main.rssrc/lib.rs 被称为 crate 根。它们之所以被称为 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
Listing 7-2: The module tree for the code in Listing 7-1

这棵树显示了一些模块如何嵌套在其他模块中;例如,hosting 嵌套在 front_of_house 中。树还显示了一些模块是 兄弟,意味着它们在同一个模块中定义;hostingserving 是在 front_of_house 中定义的兄弟模块。如果模块 A 包含在模块 B 中,我们说模块 A 是模块 B 的 子模块,而模块 B 是模块 A 的 父模块。请注意,整个模块树都根植于隐式的 crate 模块下。

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