1. 简介

Rust 的模块系统用来管理代码的组织,包括哪些内容可以被公开,哪些内容作为私有部分,以及程序每个作用域中的名字等。模块系统包括:

  • 包(Packages):Cargo 的一个功能,允许构建、测试和分享 crate。
  • crate:一个模块的树形结构,它形成了库或二进制项目。
  • 模块(Modules)和 use: 允许你控制作用域和路径的私有性。
  • 路径(path):一个命名例如结构体、函数或模块等项的方式。

2. 包和 crate

2.1 crate

crate 是一个二进制项或者库。crate root 是一个源文件,Rust 编译器以它为起始点,并构成你的 crate 的根模块。

  • 一个 crate 会将一个作用域内的相关功能分组到一起,使得该功能可以很方便地在多个项目之间共享。

2.2 包

包(package) 是提供一系列功能的一个或者多个 crate。一个包会包含有一个 Cargo.toml 文件,阐述如何去构建这些 crate。

  • 一个包中至多只能包含一个库 crate(library crate)。
  • 包中可以包含任意多个二进制 crate(binary crate)。
  • 包中至少包含一个 crate,无论是库的还是二进制的。

当我们使用 cargo new <project> 创建一个包的时候会发现,Cargo 创建的 Cargo.toml 文件中并没有提到 src/main.rs,这是因为 Cargo 遵循一个约定:

  • src/main.rs 就是一个与包同名的二进制 crate 的 crate root。同样的,Cargo 知道如果包目录中包含 src/lib.rs,则包带有与其同名的库 crate,且 src/lib.rscrate root。crate 根文件将由 Cargo 传递给 rustc 来实际构建库或者二进制项目。
  • 如果一个包同时含有 src/main.rssrc/lib.rs,则它有两个 crate:一个库和一个二进制项,且名字都与包相同。通过将文件放在 src/bin 目录下,一个包可以拥有多个二进制 crate,每个 src/bin 下的文件都会被编译成一个独立的二进制 crate。

3. 模块

模块可以将一个 crate 中的代码进行分组,以提高可读性与重用性。模块还可以控制项的「私有性」,即项是可以被外部代码使用的(public),还是作为一个内部实现的内容,不能被外部代码使用(private)。

3.1 定义

定义一个模块的基本语法格式如下:

1
2
3
mod <name> {
...
}

其中,<name> 为模块名。在模块内,还可以嵌套其他模块。通过使用模块,我们可以将相关的定义分组到一起,并指出他们为什么相关。

3.2 模块树

前面提到,src/main.rssrc/lib.rs 叫做 crate root。之所以这样叫它们是因为这两个文件的内容都分别在 crate 模块结构的根组成了一个名为 crate 的模块,该结构被称为「模块树」(module tree)。以以下模块为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mod front_of_house {
mod hosting {
fn add_to_waitlist() {}

fn seat_at_table() {}
}

mod serving {
fn take_order() {}

fn server_order() {}

fn take_payment() {}
}
}

其模块树结构为:

1
2
3
4
5
6
7
8
9
crate
└── front_of_house
├── hosting
│ ├── add_to_waitlist
│ └── seat_at_table
└── serving
├── take_order
├── serve_order
└── take_payment
  • Rust 中默认所有项(函数、方法、结构体、枚举、模块和常量)都是私有的。
  • 父模块中的项不能使用子模块中的私有项,但是子模块中的项可以使用他们父模块中的项。这是因为子模块封装并隐藏了他们的实现详情,但是子模块可以看到他们定义的上下文。
  • 可以通过使用 pub 关键字来创建公共项,使子模块的内部部分暴露给上级模块。
1
2
3
4
5
mod front_of_house {
pub mod hosting {
fn add_to_waitlist() {}
}
}

将模块分割进不同文件

比如,将 front_of_house 模块移动到属于它自己的文件 src/front_of_house.rs 中:

  • 改变 crate 根文件 src/lib.rssrc/main.rs,通过声明 front_of_house 模块告诉 Rust 在另一个与模块同名的文件中加载模块的内容:
1
mod front_of_house;	// 声明 front_of_house 模块
  • 创建一个 src/front_of_house 目录和一个包含 hosting 模块定义的 src/front_of_house/hosting.rs 文件:
1
pub fn add_to_waitlist() {}

【注】Rust 中的私有性规则不但应用于模块,还应用于结构体、枚举、函数和方法。

  • 如果我们在一个结构体定义的前面使用了 pub,这个结构体会变成公有的,但是这个结构体的字段仍然是私有的。如果要将字段公有化,则还需要分别在字段前使用 pub 关键字。
  • 如果我们将枚举设为公有,则它的所有成员都将变为公有。

3.3 路径

在 Rust 中要在模块树中找到一个项的位置,需要使用路径的方式,就像在文件系统使用路径一样。路径有两种形式:

  • 绝对路径(absolute path):从 crate 根开始,以 crate 名或者字面值 crate 开头。
  • 相对路径(relative path):从当前模块开始,以 selfsuper 或当前模块的标识符开头。

绝对路径和相对路径都后跟一个或多个由双冒号 :: 分割的标识符。

1
2
3
4
5
6
7
8
9
10
11
12
13
mod front_of_house {
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();
}

还可以使用 super 开头来构建从父模块开始的相对路径。类似于文件系统中以 .. 开头的语法。

1
2
3
4
5
6
7
8
9
10
fn serve_order() {}

mod back_of_house {
fn fix_incorrect_order() {
cook_order();
super::serve_order();
}

fn cook_order() {}
}

use 关键字引入路径

对每一个模块中的函数都使用路径来调用非常冗长且重复,并不方便。Rust 提供了 use 关键字将路径一次性引入作用域,然后就可以直接调用该路径中的项,就如同它们是本地项一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}

use crate::front_of_house::hosting; // 绝对路径
// use front_of_house::hosting; // 相对路径

pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
hosting::add_to_waitlist();
hosting::add_to_waitlist();
}
  • 作用域中使用 use 引入路径类似于在文件系统中创建软连接。
  • 通过 use 引入作用域的路径也会检查私有性,同其它路径一样。
  • 使用 use 引入函数时,习惯将函数的父模块引入到作用域,这样可以清晰地表明函数不是在本地定义的,同时使完整路径的重复度最小化。
  • 使用 use 引入结构体、枚举和其他项时,习惯直接引入它们的完整路径。

当需要引入很多定义于相同包或相同模块的项时,为每一项单独列出一行会占用源码很大的空间。相反,我们可以使用嵌套路径将相同的项在一行中引入作用域。这么做需要指定路径的相同部分,接着是两个冒号,接着是大括号中的各自不同的路径部分:

1
use std::{cmp::Ordering, io};

如果希望将一个路径下所有公有项引入作用域,可以指定路径后跟 *(glob 运算符):

1
use std::collections::*;

as 关键字指定别名

使用 use 将两个同名类型引入同一作用域可能会产生冲突。此时可以使用 as 关键字对引入的路径指定一个别名来区分彼此。

1
2
3
4
5
6
7
8
9
10
use std::fmt::Result;
use std::io::Result as IoResult;

fn function1() -> Result {
// --snip--
}

fn function2() -> IoResult<()> {
// --snip--
}

pub use 重导出

当使用 use 关键字将名称导入作用域时,在新作用域中可用的名称是私有的。 如果要让调用新作用域模块的代码也能调用 use 引入的项,可以使用 pub use 来重导出项。

1
2
3
4
5
6
7
8
9
10
11
12
13
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}

pub use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
hosting::add_to_waitlist();
hosting::add_to_waitlist();
}