【译】Rust模块系统的清晰解释

https://www.sheshbabu.com/posts/rust-module-system/

Rust的模块系统出乎意料地令人困惑,常会让初学者感到受挫。

在这篇文章中,我将使用实际例子解释模块系统使你能够清晰的理解它是如何运作的,并且能马上开始在自己的项目中使用它。

由于Rust的模块系统非常独特,我请求读者们以包容的心态读这篇文章,克制想要和其他语言的模块系统比较的想法。

我们用如下的文件目录结构来模拟真实世界的项目:

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

下面是项目依赖模块的不同路径:
1

这三个例子足以解释Rust的模块系统是怎么工作的。

例子1

从第一个例子开始:在main.rs中 导入 config.rs

1
2
3
4
5
6
7
8
9
10
// main.rs
fn main() {
println!("main");
}

// config.rs
fn print_config() {
println!("config");
}

这里每个人都会犯的第一个错就是,我们想当然地认为我们的这些config.rs,health_route.rs等文件都是模块,我们可以直接在别的文件中引用它们。

以下是我们的视角(文件系统树)和编译器的视角(模块系统树)
2
不同视角
令人意外的是,编译器只会看到 main.rs 所在的 crate ,这是由于我们必须显式的构建Rust模块树——文件系统树和模块系统树之间并没有隐式的映射。

我们必须显示构建Rust模块系统树,否则我们无法映射到文件系统树

如果想给模块系统树加一个文件,我们需要使用mod关键字把文件生命为一个子模块。另一件令人困惑事就是 你会以为我们将在同一个文件中声明子模块,实际上我们必须在不同的文件中声明!由于我们的模块系统树中仅有main.rs,我们必须把config.rs声明为main.rs的子模块。

mod关键字声明子模块

mod关键字的语法

1
2
mod my_module;

此时,编译器将在当前目录下查找my_module.rs,或者my_module/mod.rs

1
2
3
4
5
my_project
├── Cargo.toml
└─┬ src
├── main.rs
└── my_module.rs

or

1
2
3
4
5
6
my_project
├── Cargo.toml
└─┬ src
├── main.rs
└─┬ my_module
└── mod.rs

由于main.rs和config.rs在同一目录下,我们可以这样声明config模块

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

fn main() {
+ config::print_config();
println!("main");
}
// config.rs
fn print_config() {
println!("config");
}

我们通过 :: 语法访问了print_config 函数。

这是当前模块树的样子:
3

我们已经成功声明了config 模块,但是这并不足以让我们调用config.rs中的print_config。Rust中几乎一切都是默认私有的,我们必须使用pub关键字使这个函数公开。

pub关键字令事物公开

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

fn main() {
config::print_config();
println!("main");
}
// config.rs
- fn print_config() {
+ pub fn print_config() {
println!("config");
}

现在这段代码可以工作了,我们成功的调用了另一个文件中的函数。

例子2

我们这次从main.rs中调用routes/health_route.rs中定义的print_health_route。

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

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

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

如同我们之前讨论的,mod关键字只能对同一目录下my_module.rs或者my_module/mod.rs生效。

所以为了实现从main.rs调用routes/health_route.rs中的函数,我们需要做如下的事情:

创建名为routes/mod.rs,在main.rs中声明routes子模块

在routes/mod.rs中声明 health_route 子模块,并标记为公共

使 health_route.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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 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
- fn print_health_route() {
+ pub fn print_health_route() {
println!("health_route");
}

此时的模块树是这样的:
4

现在我们可以调用一个目录下的函数了。

例子3

这次我们这样调用 main.rs => routes/user_route.rs => models/user_model.rs

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/user_route.rs
fn print_user_route() {
println!("user_route");
}
// models/user_model.rs
fn print_user_model() {
println!("user_model");
}

我们将从main调用到print_user_route,从print_user_route调用到print_user_model,我们先向之前一样改,声明子模块,将函数标记为公共,添加mod文件。

1
2
3
4
5
6
7
8
9
10
11
12
my_project
├── Cargo.toml
└─┬ src
├── main.rs
├── config.rs
├─┬ routes
│ ├── mod.rs
│ ├── health_route.rs
│ └── user_route.rs
└─┬ models
+ ├── mod.rs
└── user_model.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// main.rs
mod config;
mod routes;
+ mod models;

fn main() {
routes::health_route::print_health_route();
+ routes::user_route::print_user_route();
config::print_config();
println!("main");
}
// routes/mod.rs
pub mod health_route;
+ pub mod user_route;
// routes/user_route.rs
- fn print_user_route() {
+ pub fn print_user_route() {
println!("user_route");
}
// models/mod.rs
+ pub mod user_model;
// models/user_model.rs
- fn print_user_model() {
+ pub fn print_user_model() {
println!("user_model");
}

现在模块树看起来是这样的:
5

等等,我们还没真正从 print_user_route 中调用 print_user_model 呢!到目前为止,我们只在main.rs中调用过其他模块中声明的function,我们怎么做到在其他文件中调用呢?

如果我们查看模块树会发现,print_user_model函数在 crate::models::user_model 这个路径上,所以如果我们想用非main.rs 中的模块,我们应考虑访问模块树中此模块的路径。

1
2
3
4
5
6

// routes/user_route.rs
pub fn print_user_route() {
+ crate::models::user_model::print_user_model();
println!("user_route");
}

目前我们成功的从非main.rs中调用别的文件中的模块。

super

如果我们文件组织存在多个深层目录,(模块的)全限定名就会很长。假定我们想从print_user_route 中调用 print_health_route 。它们分别在 crate::routes::health_route ,crate::routes::user_route路径下。

我们可以通过全限定名 crate::routes::health_route::print_health_route() 来调用,我们也可以用相对路径super::health_route::print_health_route();来调用 。请注意,此处我们使用super关键字来引用父模块。

模块中super关键字指向父模块

1
2
3
4
5
6
7
pub fn print_user_route() {
crate::routes::health_route::print_health_route();
// 也可以这么写
super::health_route::print_health_route();

println!("user_route");
}

use

使用上面例子中全限定名甚至相对路径代码会十分冗长。为了缩短这些路径,我们可以用use关键字将此路径和一个新的名称或别名绑定。

use关键字可以用于缩短模块路径

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

上面的路径可以改为

1
2
3
4
5
6
use crate::models::user_model::print_user_model;

pub fn print_user_route() {
print_user_model();
println!("user_route");
}

我们也可以用别名代替 print_user_model 

1
2
3
4
5
6
use crate::models::user_model::print_user_model as log_user_model;

pub fn print_user_route() {
log_user_model();
println!("user_route");
}

External modules(外部模块)

加到Cargo.toml中的依赖是项目中所有模块中全局可用的。我们无需显式导入或声明什么就可以使用这个依赖了

外部依赖对项目中所有模块全部可见

例如,我们可以把rand这个crate加到我们的项目里,我们可以像这样直接在代码里调用

1
2
3
4
5
pub fn print_health_route() {
let random_number: u8 = rand::random();
println!("{}", random_number);
println!("health_route");
}

我们也可以缩短这个路径

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");
}

总结

  • 模块系统是显式的,不会和文件系统一对一映射。
  • 我们在父模块声明声明一个文件为模块
  • mod关键字用于声明子模块
  • 我们需要显式声明函数,结构体为公共(public) ,别的模块才能使用 它
  • pub关键字可以声明公共
  • use关键字可以缩短模块路径
  • 我们不需要显式声明三方模块