当你编写大型程序时,组织你的代码显得尤为重要,因为你想在脑海中通晓整个程序,那几乎是不可能完成的。通过对相关功能进行分组和划分不同功能的代码,你可以清楚在哪里可以找到实现了特定功能的代码,以及在哪里可以改变一个功能的工作方式。
伴随着项目的增长,你可以通过将代码分解为多个模块和多个文件来组织代码。一个包可以包含多个二进制 crate 项和一个可选的 crate 库。伴随着包的增长,你可以将包中的部分代码提取出来,做成独立的 crate,这些 crate 则作为外部依赖项。本章将会涵盖所有这些概念。对于一个由一系列相互关联的包组合而成的超大型项目,Cargo 提供了 “工作空间” 这一功能。
除了对功能进行分组以外,封装实现细节可以使你更高级地重用代码:你实现了一个操作后,其他的代码可以通过该代码的公共接口来进行调用,而不需要知道它是如何实现的。你在编写代码时可以定义哪些部分是其他代码可以使用的公共部分,以及哪些部分是你有权更改实现细节的私有部分。这是另一种减少你在脑海中记住项目内容数量的方法。
这里有一个需要说明的概念 “作用域(scope)”:代码所在的嵌套上下文有一组定义为 “in scope” 的名称。当阅读、编写和编译代码时,开发者和编译器需要知道特定位置的特定名称是否引用了变量、函数、结构体、枚举、模块、常量或者其他有意义的项。 你可以创建作用域,以及改变哪些名称在作用域内还是作用域外。同一个作用域内不能拥有两个相同名称的项;可以使用一些工具来解决名称冲突。
Rust 有许多功能可以让你管理代码的组织,包括哪些内容可以被公开,哪些内容作为私有部分,以及程序每个作用域中的名字。这些功能。这有时被称为 “模块系统(the module system)”,包括:
- 包(Package): Cargo 的一个功能是它允许我们构建、测试和分享 crate。
- Crates: 一个模块的树形结构,他形成了库或二进制项目。
- 模块(Modules)和 use: 允许我们控制作用域和路径的私有性。
- 路径(path): 一个命名例如结构体、函数或模块等项的方式。
本章将会涵盖所有这些概念,讨论它们如何交互,并说明如何使用它们来管理作用域。到最后,你会对模块系统有深入的了解,并且能够像专业人士一样使用作用域!
包和 crate
模块系统的第一部分,我们将介绍包和 crate。crate 是一个二进制项或者库。crate root 是一个源文件,Rust 编译器以它为起始点,并构成你的 crate 的根模块。*包(package)*是提供一系列功能的一个或者多个 crate。一个包会包含有一个 Cargo.toml 文件,阐述如何去构建这些 crate。
包中所包含的内容由几条规则来确立。一个包中至多 只能 包含一个库 crate(library crate);包中可以包含任意多个二进制 crate(binary crate);包中至少包含一个 crate,无论是库的还是二进制的。
让我 们来看看创建包的时候会发生什么。首先,我们输入命令 cargo new
:
cargo new my-project
当我们输入了这条命令,Cargo 会给我们的包创建一个 Cargo.toml 文件。查看 Cargo.toml 的内容,会发现并没有提到 src/main.rs,因为 Cargo 遵循的一个约定:src/main.rs 就是一个与包同名的二进制 crate 的 crate 根。同样的,Cargo 知道如果包目录中包含 src/lib.rs,则包带有与其同名的库 crate,且 src/lib.rs 是 crate 根。crate 根文件将由 Cargo 传递给 rustc
来实际构建库或者二进制项目。
在此,我们有了一个只包含 src/main.rs 的包,意味着它只含有一个名为 my-project
的二进制 crate。如果一个包同时含有 src/main.rs 和 src/lib.rs,则它有两个 crate:一个库和一个二进制项,且名字都与包相同。通过将文件放在 src/bin 目录下,一个包可以拥有多个二进制 crate:每个 src/bin 下的文件都会被编译成一个独立的二进制 crate。
一个 crate 会将一个作用域内的相关功能分组到一起,使得该功能可以很方便地在多个项目之间共享。
将一个 crate 的功能保持在其自身的作用域中,可以知晓一些特定的功能是在我们的 crate 中定义的还是在 rand
crate 中定义的,这可以防止潜在的冲突。例如,rand
crate 提供了一个名为 Rng
的特性(trait)。我们还可以在我们自己的 crate 中定义一个名为 Rng
的 struct
。因为一个 crate 的功能是在自身的作用域进行命名的,当我们将 rand
作为一个依赖,编译器不会混淆 Rng
这个名字的指向。在我们的 crate 中,它指向的是我们自己定义的 struct Rng
。我们可以通过 rand::Rng
这一方式来访问 rand
crate 中的 Rng
特性(trait)。
定义模块来控制作用域私有性
模块 让我们可以将一个 crate 中的代码进行分组,以提高可读性与重用性。模块还可以控制项的 私有性,即项是可以被外部代码使用的*(public),还是作为一个内部实现的内容,不能被外部代码使用(private)*。
在餐饮业,餐馆中会有一些地方被称之为 前台(front of house),还有另外一些地方被称之为 后台(back of house)。前台是招待顾客的地方,在这里,店主可以为顾客安排座位,服务员接受顾 客下单和付款,调酒师会制作饮品。后台则是由厨师工作的厨房,洗碗工的工作地点,以及经理做行政工作的地方组成。
我们可以将函数放置到嵌套的模块中,来使我们的 crate 结构与实际的餐厅结构相同。通过执行 cargo new --lib restaurant
,来创建一个新的名为 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() {}
}
}
示例 7-1:一个包含着含有函数的其他模块们的 front_of_house
模块
我们用关键字 mod
定义一个模块,指定模块的名字(在示例中为 front_of_house
),并用大括号包围模块的主体。我们可以在模块中包含其他模块,就像本示例中的 hosting
和 serving
模块。模块中也可以包含其他项,比如结构体、枚举、常量、trait,或者像示例 7-1 一样——包含函数。
通过使用模块,我们可以把相关的 定义组织起来,并通过模块命名来解释为什么它们之间有相关性。使用这部分代码的开发者可以更方便的循着这种分组找到自己需要的定义,而不需要通览所有。编写这部分代码的开发者通过分组知道该把新功能放在哪里以便继续让程序保持组织性。
之前我们提到,src/main.rs
和 src/lib.rs
被称为 crate 根。如此称呼的原因是,这两个文件中任意一个的内容会构成名为 crate
的模块,且该模块位于 crate 的被称为 模块树 的模块结构的根部("at the root of the crate’s module structure")。
示例 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
的隐式模块下。
模块树或许让你想起了电脑上文件系统的目录树。这是一个非常恰当的比喻!就像文件系统中的目录那样,你应使用模块来组织你的代码。而且就像一个目录中的文件那样,我们需要一个找到我们的模块的方式。
路径用于引用模块树中的项
在 Rust 中我们使用路径的方式,就像在文件系统使用路径一样。如果我们想要调用一个函数,我们需要知道它的路径。
路径有两种形式:
- 绝对路径(*absolute path)*从 crate 根部开始,以 crate 名或者字面量
crate
开头。 - 相对路径*(relative path)*从当前模块开始,以
self
、super
或当前模块的标识符开头。
绝对路径和相对路径都后跟一个或多个由双冒号(::
)分割的标识符。
让我们回到示例 7-1。我们如何调用 add_to_waitlist
函数?还是同样的问题,add_to_waitlist
函数的路径是什么?在示例 7-3 中,我们通过删除一些模块和函数,稍微简化了一下我们的代码。我们在 crate 根部定义了一个新函数 eat_at_restaurant
,并在其中展示调用 add_to_waitlist
函数的两种方法。eat_at_restaurant
函数是我们 crate 库的一个公共 API,所以我们使用 pub
关键字来标记它。
文件名: src/lib.rs
mod front_of_house {
mod hosting {
fn add_to_waitlist() {}
}
}
fn eat_at_restaurant() {
// 绝对路径
crate::front_of_house::hosting::add_to_waitlist();
// 相对路径
front_of_house::hosting::add_to_waitlist();
}
示例 7-3: 使用绝对路径和相对路径来调用 add_to_waitlist
函数
第一种方式,我们在 eat_at_restaurant
中调用 add_to_waitlist
函数,使用的是绝对路径。add_to_waitlist
函数与 eat_at_restaurant
被定义在同一 crate 中,这意味着我们可以使用 crate
关键字为起始的绝对路径。
在 crate
后面,我们持续地嵌入模块,直到我们找到 add_to_waitlist
。你可以想象出一个相同结构的文件系统,我们通过指定路径 /front_of_house/hosting/add_to_waitlist
来执行 add_to_waitlist
程序。我们使用 crate
从 crate 根部开始就类似于在 shell 中使用 /
从文件系统根开始。
第二种方式,我们在 eat_at_restaurant
中调用 add_to_waitlist
,使用的是相对路径。这个路径以 front_of_house
为起始,这个模块在模块树中,与 eat_at_restaurant
定义在同一层级。与之等价的文件系统路径就是 front_of_house/hosting/add_to_waitlist
。以名称为起始,意味着该路径是相对路径。
选择使用相对路径还是绝对路径,还是要取决于你的项目。取决于你是更倾向于将项的定义代码与使用该项的代码分开来移动,还是一起移动。举一个例子,如果我们要将 front_of_house
模块和 eat_at_restaurant
函数一起移动到一个名为 customer_experience
的模块中,我们需要更新 add_to_waitlist
的绝对路径,但是相对路径还是可用的。然而,如果我们要将 eat_at_restaurant
函数单独移到一个名为 dining
的模块中,还是可以使用原本的绝对路径来调用 add_to_waitlist
,但是相对路径必须要更新。我们更倾向于使用绝对路径,因为把代码定义和项调用各自独立地移动是更常见的。
让我们试着编译一下示例 7-3,并查明为何不能编译!示例 7-4 展示了这个错误。
PS E:\github\rust-projects\restaurant> cargo build
Compiling restaurant v0.1.0 (E:\github\rust-projects\restaurant)
error[E0603]: module `hosting` is private
--> src\lib.rs:9:28
|
9 | crate::front_of_house::hosting::add_to_waitlist();
| ^^^^^^^ --------------- function `add_to_waitlist` is not publicly re-exported
| |
| private module
|
note: the module `hosting` is defined here
--> src\lib.rs:2:5
|
2 | mod hosting {
| ^^^^^^^^^^^
error[E0603]: module `hosting` is private
--> src\lib.rs:12:21
|
12 | front_of_house::hosting::add_to_waitlist();
| ^^^^^^^ --------------- function `add_to_waitlist` is not publicly re-exported
| |
| private module
|
note: the module `hosting` is defined here
--> src\lib.rs:2:5
|
2 | mod hosting {
| ^^^^^^^^^^^
For more information about this error, try `rustc --explain E0603`.
error: could not compile `restaurant` (lib) due to 2 previous errors
示例 7-4: 构建示例 7-3 出现的编译器错误
错误信息说 hosting
模块是私有的。换句话说,我们拥有 hosting
模块和 add_to_waitlist
函数的的正确路径,但是 Rust 不让我们使用,因为它不能访问私有片段。
模块不仅对于你组织代码很有用。他们还定义了 Rust 的 私有性边界(privacy boundary):这条界线不允许外部代码了解、调用和依赖被封装的实现细节。所以,如果你希望创建一个私有函数或结构体,你可以将其放入模块。
Rust 中默认所有项(函数、方法、结构体、枚举、模块和常量)都是私有的。父模块中的项不能使用子模块中的私有项,但是子模块中的项可以使用他们父模块中的项。这是因为子模块封装并隐藏了他们的实现详情,但是子模块可以看到他们定义的上下文。 继续拿餐馆作比喻,把私有性规则想象成餐馆的后台办公室:餐馆内的事务对餐厅顾客来说是不可知的,但办公室经理可以洞悉其经营的餐厅并在其中做任何事情。
Rust 选择以这种方式来实现模块系统功能,因此默认隐藏内部实现细节。这样一来,你就知道可以更改内部代码的哪些部分而不会破坏外部代码。你还可以通过使用 pub 关键字来创建公共项,使子模块的内部部分暴露给上级模块。
使用 pub 关键字暴露路径
让我们回头看一下示例 7-4 的错误,它告诉我们 hosting
模块是私有的。我们想让父模块中的 eat_at_restaurant
函数可以访问子模块中的 add_to_waitlist
函数,因此我们使用 pub
关键字来标记 hosting
模块,如示例 7-5 所示。
文件名: src/lib.rs
pub mod front_of_house {
pub mod hosting {
fn add_to_waitlist() {}
}
}
pub fn eat_at_restaurant() {
// 绝对路径
crate::front_of_house::hosting::add_to_waitlist();
// 相对路径
front_of_house::hosting::add_to_waitlist();
}
示例 7-5: 使用 pub
关键字声明 hosting
模块使其可在 eat_at_restaurant
使用
不幸的是,示例 7-5 的代码编译仍 然有错误,如示例 7-6 所示。
PS E:\github\rust-projects\restaurant> cargo build
Compiling restaurant v0.1.0 (E:\github\rust-projects\restaurant)
error[E0603]: function `add_to_waitlist` is private
--> src\lib.rs:9:37
|
9 | crate::front_of_house::hosting::add_to_waitlist();
| ^^^^^^^^^^^^^^^ private function
|
note: the function `add_to_waitlist` is defined here
--> src\lib.rs:3:9
|
3 | fn add_to_waitlist() {}
| ^^^^^^^^^^^^^^^^^^^^
error[E0603]: function `add_to_waitlist` is private
--> src\lib.rs:12:30
|
12 | front_of_house::hosting::add_to_waitlist();
| ^^^^^^^^^^^^^^^ private function
|
note: the function `add_to_waitlist` is defined here
--> src\lib.rs:3:9
|
3 | fn add_to_waitlist() {}
| ^^^^^^^^^^^^^^^^^^^^
For more information about this error, try `rustc --explain E0603`.
error: could not compile `restaurant` (lib) due to 2 previous errors
示例 7-6: 构建示例 7-5 出现的编译器错误
发生了什么?在 mod hosting
前添加了 pub
关键字,使其变成公有的。伴随着这种变化,如果我们可以访问 front_of_house
,那我们也可以访问 hosting
。但是 hosting
的 内容(contents) 仍然是私有的;这表明使模块公有并不使其内容也是公有的。模块上的 pub
关键字只允许其父模块引用它。
示例 7-6 中的错误说,add_to_waitlist
函数是私有的。私有性规则不但应用于模块,还应用于结构体、枚举、函数和方法。
让我们继续将 pub
关键字放置在 add_to_waitlist
函数的定义之前,使其变成公有。如示例 7-7 所示。
文件名: src/lib.rs
pub mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
pub fn eat_at_restaurant() {
// 绝对路径
crate::front_of_house::hosting::add_to_waitlist();
// 相对路径
front_of_house::hosting::add_to_waitlist();
}