键盘快捷键

按 ← 或 → 在章节间切换

按 S 或 / 搜索本书

按 ? 显示此帮助

按 Esc 隐藏此帮助

使用模块控制作用域和隐私性

本节将讨论模块及模块系统的其他部分:用于为项目命名的路径、将路径引入作用域的 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,其内容如下:

文件名: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 模块包含其他模块,而这些模块中又包含函数
they’re related. Programmers using this code can navigate the code based on the groups rather than having to read through all the definitions, making it easier to find the definitions relevant to them. Programmers adding new functionality to this code would know where to place the code to keep the program organized.

我们使用关键字 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 中代码的模块树
you use modules to organize your code. And just like files in a directory, we

这棵树展示了模块如何嵌套在其他模块中;例如,hosting 嵌套在 front_of_house 中。树还展示了一些模块是同级模块,也就是说它们定义在同一个模块中;hosting 和 serving 就是定义在 front_of_house 中的同级模块。如果模块 A 位于模块 B 内部,我们就称模块 A 是模块 B 的子模块,模块 B 是模块 A 的父模块。请注意,整个模块树都以隐式模块 crate 为根。

模块树可能会让你想到计算机文件系统中的目录树;这是一个非常贴切的类比!就像用文件系统中的目录组织文件一样,我们使用模块组织代码。正如我们需要一种方式查找目录中的文件,也需要一种方式查找模块。