按照不同的特性来组织或者分割相关功能的代码,能够让我们清晰地找到实现指定功能的代码片段。一个包(package)可以拥有多个二进制单元及一个可选的库单元包。代码也可以拆分到独立的单元包(crate)中,并将它作为外部依赖进行引用。除了对功能进行分组外,对实现细节进行封装可以让你在更高层次上复用代码。另一个与组织和封装密切相关的概念是作用域(scope)。
Rust 提供了一系列功能来帮助我们管理代码,这些功能有时被称为模块系统(module system),它们包括:
- 包(package):一个用于构建、测试并分享单元包的 Cargo 功能
- 单元包(crate):一个用于生成库或者可执行文件的树形模块结构
- 模块(module)及 use 关键字:被用于控制文件结构、作用域以及路径的私用性
- 路径(path):一种用于命名条目的方法,这些条目包括结构体、函数和模块等
包与单元包
通过定义模块来控制作用域及私有性
模块允许我们将单元包内的代码按照可读性与易用性来进行分组,同时它还允许我们控制条目的私有性。模块决定了一个条目是否可以被外部代码使用(公用),或者仅仅是一个内部的实现细节而不对外暴露(私有)。
如下创建了一个 restaurant
的库:cargo new --lib restaurant
1 | mod front_of_house { |
- 以 mod 关键字开头来定义一个模块,接着指明这个模块的名字,并在其后使用一对
{}
来包裹模块体 - 模块体内可以继续定义其他模块,模块内同样也可以包含其他条目的定义,例如结构体、枚举、常量等等
src/main.rs
与 src/lib.rs
称为单元包的根节点,因为这两个文件各自组成了一个名为 crate
的模块,并位于单元包模块结构的根部。这个模块结构也称为模块树。整个模块树都被放置在一个名为 crate
的隐式根模块下。如下展示了上述代码的树状模块结构:
![](/fuchencong.github.io/2023/03/29/rust-programming-language-06/images/module_tree.jpg)
用于在模块树中指明条目的路径
为了在 Rust 中模块树中找到某个条目,同样需要使用路径。路径有两种形式:
- 使用单元包名或字面量 crate 从根节点开始的绝对路径
- 使用 self、super 或者内部标识符从当前模块开始的相对路径
标识符之间使用双冒号 ::
分隔。大部分 Rust 开发者更倾向于使用绝对路径,因为我们往往会彼此独立地移动代码的定义与调用代码。
模块不仅用于组织代码,同时还定义了 Rust 中的私有边界:外部代码无法知晓、调用或依赖那些由私有边界封装了的实现细节。Rust 中所有条目(函数、方法、结构体、枚举、模块和常量),处于父级模块中的条目无法使用子级模块中的私有条目,但子模块的条目可以使用它所有祖先模块的条目。虽然子模块包装并隐藏了自身的实现细节,但是它却依然能够感知当前定义环境中的上下文。
同样可以从父模块开始构造相对路径,这一方式需要在路径起始处使用 super 关键字。
使用 pub 关键字来暴露路径
可以使用 pub 关键字来将某些条目标记为公共的,从而使子模块中的这些部分暴露到祖先模块中。但是将模块变为公共状态并不会影响到它内部条目的状态,模块之前的 pub 关键字仅仅意味着祖先模块拥有了指向该模块的权限。私有性规则不仅作用于模块,也同样作用于结构体、枚举、函数及方法。
1 | mod front_of_house { |
这个例子中,由于 eat_at_restaurant
函数被定义在与 front_of_module
相同的模块中(即 eat_at_restaurant
与 front_of_house
属于同级节点),因此可以直接在 eat_at_restaurant
中引用 front_of_house
。
当我们在结构体定义前使用 pub 时,结构体本身就成为公共结构体,但是它的字段依旧保持了私有状态。我们可以逐一决定是否将某个字段公开。但是当我们将一个枚举声明为公共时,它所有的变体都自动变为了公共状态。
使用 use 关键字将路径导入作用域中
使用路径来调用函数的写法看上去会有些重复冗长,我们可以借助 use 关键字来将路径引入作用域中,并像使用本地条目一样来调用路径中的条目。
例如如果在单元包的根节点下使用 use crate::front_of_house::hosting
,则 hosting
成为当前作用域下一个有效名称,就如同 hosting 模块被定义在根节点下一样。使用 use 将路径引入作用域时也同样需要遵守私有性规则。
使用 use 来指定相对路径则稍有些不同,必须在传递给 use 的路径开始处使用关键字 self,而不是从当前作用域可用的名称开始。
通常我们会将函数的父模块引入作用域中,而不是直接将函数引入作用域中。这样我们必须在调用函数时指定这个父模块,从而更清晰地表明当前函数没有被定义在当前作用域中。但是当使用 use 将结构体、枚举或其他条目引入作用域时,习惯于通过指定完整路径的方式引入。这是一种约定俗称的习惯,被开发者广泛应用于 Rust 代码中。
但是如果我们需要将两个拥有相同名称的条目引入作用域时,可能就必须使用它们的父模块,从而区分出不同的类型。另外一种解决方案是:我们可以在路径后面使用 as 关键字为类型指定一个新的本地名称,也就是别名。
当我们使用 use 关键字将名称引入作用域时,该名称会以私有方式在新的作用域中生效。为了让外部代码能够访问这些名称,可以通过组合使用 pub 与 use 实现。该技术也称为重导出(re-exporting)。因为我们不仅将条目引入了作用域,而且使该条目可以被外部代码从新的作用域引入自己的作用域。
通过使用 pub use
,可以在编写代码时使用一种结构,而在对外暴露时使用另外一种结构。这一方法可以让我们的代码库编写者与调用者同时保持良好的组织结构。
使用外部包
为了使用外部包,首先需要将它们列入 Cargo.toml 文件,之后我们以 use + 包名
并在包名后列出我们想要引入作用域的条目,来将外部包的条目引入当前作用域。标准库实际上也是当前项目的外部包,由于标准库已经被内置到了 Rust 语言中,所以不需要特意修改 Cargo.toml
来包含 std。但是仍然需要使用 use 来将标准库中特定条目引入当前项目的作用域。
使用嵌套路径来清理众多 use 语句
当想要使用同一个包或同一个模块内的众多条目时,将它们逐行列出会比较繁琐,也会占据较多纵向空间。可以在同一行内使用嵌套路径来将多个条目引入作用域。此时首先指定路径的相同部分,再在后面跟上两个冒号,接着使用一对花括号包裹路径差异部分的列表。使用嵌套路径来将众多条目从同一个包或同一个模块引入作用域可以节省大量的独立 use 语句。
1 | use std::cmp::Ordering; |
1 | use std::{cmp::Ordering, io}; |
可以在路径的任意层级使用嵌套路径,这一特性对于合并两行共享子路径的 use 语句十分有用。
1 | use std::io; |
1 | use std::io::{self, Write}; |
通配符
如果想要将所有定义在某个路径上中的公共条目都导入作用域,可以在指定路径时使用 *
通配符,例如 use std::collections::*
。需要谨慎使用该特性,通配符会使你难以确定作用域中存在哪些名称,以及某个名称的具体定义位置。
将模块拆分到不同文件
当模块逐渐增大时,可以将它们的定义移动到新的文件中,从而使代码更加易于浏览。举个例子,在 src/lib.rs
使用如下代码:
1 | mod front_of_house; |
在 mod front_of_house
后使用分号而不是代码块会让 Rust 前往与当前模块同名的文件中加载模块内容。该规则不仅适用于根节点文件 src/lib.rs
,也可以被应用到以 src/main.rs
为根节点的二进制单元包中。我们使用 mod
关键字声明模块,并指示 Rust 在同名文件中搜索模块内的代码。
例如重新按照如下方式组织代码结构:
src/lib.rs
内容如下,声明 front_of_house
模块,其代码位于 src/front_of_house.rs
1 | mod front_of_house; |
src/front_of_house.rs
内容如下,其又声明了 hosting
模块,其代码位于 src/front_of_house/hosting.rs
1 | pub mod hosting; |
src/front_of_house/hosting.rs
内容如下:
1 | pub fn add_to_waitlist() {} |
以上修改并不会修改原有的模块树结构,尽管这些定义被放置到了不同的文件中。该方法使我们可以在模块规模逐渐增大时将它们移动至新的文件中。
另外还有一点需要注意,如果一个目录下有一个名为 mod.rs
的源文件,模块名也可以映射到目录名。所以模块路径 front_of_house::hosting
既可以映射为 front_of_house/hosting.rs
(在 hosting.rs 中定义模块内容),也可以映射为 front_of_house/hosting/mod.rs
(在 mod.rs 中定义模块内容)。但是 front_of_house/hosting.rs
和 front_of_house/hosting/mod.rs
不能同时存在。