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

在本节中,我们将讨论模块和模块系统的其他部分,即路径,它允许你命名项目;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)来定义一个模块。模块的主体随后进入花括号内。在模块内部,我们可以放置其他模块,例如本例中的模块 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

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

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

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