Rust's Module System

Original post: http://www.sheshbabu.com/posts/rust-module-system/

Rust的模块系统比较特殊,初学者不太容易上手理解,上面的文章详细介绍了Rust模块系统,下面是阅读笔记。

工程结构

首先,我们看一个典型的Rust工程的文件结构,和其中的调用

image-20210721103030139

Example 1

首先看第一个例子:从main.rs中调用config.rs。在Rust中,每一个文件或文件夹都被看做一个module,如config.rs,实际上就是一个名为config的module。我们可以从其他的文件中导入该module。在最初没有任何导入语句的时候,Rust编译器只能看到如下的结构:

image-20210721103513662

默认情况下,Rust的编译器只会把main.rs看做是crate模块,而不会根据文件目录自动生成模块树,所有的模块结构都需要我们手动导入构建。

那么,接下来我们就手动构建我们的模块树。在Rust中,关键字mod被用来声明一个模块或者子模块。比如,对于config.rs,我们需要使用mod config;来声明该模块。

容易出错的是,我们不是在config.rs中使用mod config;声明,而是需要在main.rs中使用mod config;去声明config模块:

1
2
// main.rs
mod config; // declare config mod

main.rs中声明了config模块之后,Rust的编译器就会去寻找config.rs文件或者config/mod.rs文件来构建模块结构。如果config.rs中有一个名为print_config()的函数,那么就可以通过config::print_config()来调用了。下面是完整的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// main.rs
mod config;

fn main() {
  config::print_config();
  println!("main");
}

// config.rs
pub fn print_config() {
  // 注意需要把函数声明为public
  println!("config");
}

到此为止我们学会了同级的模块导入。但是在实际工程中,我们往往希望通过文件夹来把一个系列的文件组织到一起。下面看第二个例子。

Example 2

假设我们需要从main.rs中调用routes/health_route.rs中的函数print_health_route。由于health_route.rs在文件夹routes下面,因此这个文件对main.rs来说是不可见的。如何解决呢?这个时候,我们就需要在routes文件夹下面添加一个名为mod.rs的文件。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
my_project
├── Cargo.toml
└─┬ src
  ├── main.rs
  ├── config.rs
  ├─┬ routes
+ │ ├── mod.rs
  │ ├── health_route.rs
  │ └── user_route.rs
  └─┬ models
    └── user_model.rs

mod.rs在Rust的模块系统中非常重要。还记的上面的config.rs吗?如果config模块的代码越来越多,需要分成若干个文件进行组织的时候,就需要创建config文件夹,然后在config/mod.rs中声明文件夹下的所有子模块了。可以这样认为:文件夹模块的config/mod.rs其实就相当于单文件模块的config.rs

在本例子中,也是一样的。想要在main.rs中调用print_health_route(),我们需要做如下三件事:

  1. 创建routes/mod.rs,并且在main.rs中,使用mod routes;声明routes模块
  2. routes/mod.rs中声明health_route子模块,并且把它声明为public
  3. routes/health_route.rs中,把函数print_health_route()声明为public

完成的代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// main.rs
mod config;
mod routes;

fn main() {
  routes::health_route::print_health_route();
  config::print_config();
  println!("main");
}

// routes/mod.rs
pub mod health_route;

// routes/health_route.rs
pub fn print_health_route() {
  println!("health_route");
}

此时的模块树如下所示:

image-20210721105947578

Example 3

第三个例子中,我们会尝试如下的调用链路:main.rs => routes/user_route.rs => models/user_model.rs

首先和Example 2中的一样,我们把models/user_model.rs子模块构建好:

image-20210721110324269

然后,我们看调用链路的后半部分:从routes/user_route.rs中调用models/user_model/rs中的函数。对于同级的函数调用,我们可以从模块树的最上面逐级往下调用,即crate::models::user_model

1
2
3
4
5
// routes/user_route.rs
pub fn print_user_route() {
  crate::models::user_model::print_user_model();
  println!("user_route");
}

但是,在文件路径非常长的时候,每次从根模块往下找路径会非常冗长,因此Rust提供了super关键词来定位到父模块。

如果我想想要从user_route.rs中调用health_route,那么可以使用super::health_route::print_health_route()代替crate::routes::health_route::print_health_route

1
2
3
4
5
6
7
pub fn print_user_route() {
  // crate::routes::health_route::print_health_route();
  // can also be called using
  super::health_route::print_health_route();

  println!("user_route");
}

Use关键字

在调用其他模块时,大部分时候不会每次都写完整的调用链路,而是会使用use关键字先导入某个模块。就上面的路子,可以改成如下的代码:

1
2
3
4
5
6
7
8
// 导入
use crate::routes::health_route::print_health_route;

pub fn print_user_route() {
  // 使用
  print_health_route();
  println!("user_route");
}

三方包

在使用三方包时,需要在Cargo.toml中声明[dependencies]。声明之后的三方包模块会在工程所有的模块中自动生效。

举例:如果我们在[dependencies]中使用了rand包,在任意一个文件中,我们可以直接使用该模块:

1
2
3
4
5
6
pub fn print_health_route() {
  // 直接使用即可
  let random_number: u8 = rand::random();
  println!("{}", random_number);
  println!("health_route");
}

当然,我们也可以先use,然后再使用:

1
2
3
4
5
6
7
use rand::random;

pub fn print_health_route() {
  let random_number: u8 = random();
  println!("{}", random_number);
  println!("health_route");
}

总结

  1. Rust中的模块系统需要显式地去声明结构
  2. 在声明一个模块时,需要在它的父级声明,而不是在这个文件本身(上面的main.rs相当于是config.rs的父级,同样,mod.rs也是相当于是同级的health_score.rs的父级)
  3. mod关键字用于声明子模块
  4. 如果想要一个函数、结构体等被其他模块使用,必须使用pub关键字声明其为public
  5. 使用use关键字可以导入模块以减少重复代码
  6. 三方包不必显式地声明
updatedupdated2024-05-102024-05-10