您的当前位置:首页正文

Rust模块系统的清晰解释

2024-11-20 来源:个人技术集锦

Rust的模块系统(module system)容易让人费解,会让新手感受到挫败感。

这篇博客,我将通过真实的例子来解释模块系统,让你对模块系统是如何运行的有一个清晰的理解,并且能立刻应用到你的项目中。

由于Rust的模块系统比较特殊,我希望读者能够虚心阅读这篇博客并且不要将Rust的模块系统和其它编程语言的模块工作原理进行对比。

让我们使用下面的文件结构来模拟真实的项目:

my_project
├── Cargo.toml
└─┬ src
  ├── main.rs
  ├── config.rs
  ├─┬ routes
  │ ├── health_route.rs
  │ └── user_route.rs
  └─┬ models
    └── user_model.rs

有不同的方式让我们能够使用我们的模块:

这三个例子(每种颜色的线条和依赖关系代表一种使用方式)足够我们来解释Rust模块系统是如何工作的。

例子1

第一个例子,让我们在main.rs中导入config.rs:

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

第一个基本上每个人都会犯的错误是因为我们有类似config.rs,health_routes.rs等等文件,我们认为这些文件就是模块,并且我们可以从在其它文件中导入它们。

下面是我们从文件系统角度看到的文件树和编译器角度看到的模块树:

令人惊讶的是,编译器仅仅能够看到crate模块,那就是我们的main.rs文件。这是因为在Rust中,我们需要显式地构建模块树——文件系统树和模块树之间没有隐式(implicit)的映射(mapping)。

在Rust中,我们需要显式地(explicitly)构建模块树,模块结构和文件结构之间没有隐式(implicit)的映射关系!

为了将一个文件添加到模块树中,我们需要通过mod关键字将文件声明为子模块(submodule)。下一个让人感到困惑的是,你会认为我们在同一个文件中将文件声明为模块,其实我们需要在不同的文件中去声明一个文件作为模块!目前我们只有一个main.rs文件在模块树中,让我们在main.rs中将config.rs文件声明为子模块(submodule)。

mod关键字用于声明一个子模块(submodule)

mod关键字的语法是:

mod my_module;
my_project
├── Cargo.toml
└─┬ src
  ├── main.rs
  └── my_module.rs

or

my_project
├── Cargo.toml
└─┬ src
  ├── main.rs
  └─┬ my_module
    └── mod.rs
// main.rs
+ mod config;

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

文件中+号后边跟着的代码代表新增的代码,-号后边跟着的代码代表删除的旧代码。就是通过git diff看到的文件变更详情。

我们在main函数中通过::语法使用config.rs中定义的print_config函数。下面是模块树的样子:

我们已经成功地声明了config模块!但是,这还不足够让我们能够调用config.rs中的print_config方法。因为在Rust中,几乎所有的东西默认都是私有的(private by default),我们需要使用pub关键字来让函数变成公开(public)的:

pub关键字让事物变成公开的

// main.rs
mod config;

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

在fn前面加上pub,可以让这个函数变成public的。

现在,我们可以调用print_config函数了。我们成功地调用了一个在不同文件中定义的函数了!

例子2

让我们尝试在main.rs中调用在routes/health_route.rs中定义的print_health_route函数:

// main.rs
mod config;

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

所以,为了在main.rs中调用routes/health_route.rs中定义的函数,我们需要做下面的事情:

  • 创建一个叫做routes/mod.rs的文件,并且在main.rs中声明routes子模块。
  • routes/mod.rs中声明health_route子模块,并且让它是public的
  • health_route.rs中的函数变成public的
my_project
├── Cargo.toml
└─┬ src
  ├── main.rs
  ├── config.rs
  ├─┬ routes
+ │ ├── mod.rs
  │ ├── health_route.rs
  │ └── user_route.rs
  └─┬ models
    └── user_model.rs
// 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");
}

模块树将变成下面这样的:

例子3

让我们尝试通过main.rs => routes/user_route.rs => modules/user_model.rs这样的调用链进行调用:

// 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

让我们像之前做过的更改一样对文件进行修改——声明子模块,让函数变成public并且声明mod.rs文件:

my_project
├── Cargo.toml
└─┬ src
  ├── main.rs
  ├── config.rs
  ├─┬ routes
  │ ├── mod.rs
  │ ├── health_route.rs
  │ └── user_route.rs
  └─┬ models
+   ├── mod.rs
    └── user_model.rs
// 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");
}

模块树将如下所示:

等一下,我们还没有真正地从print_user_route函数中调用print_user_model!到目前为止,我们只在main.rs中调用了其它模块中定义的函数,我们怎么在其它文件中调用其它模块中定义的函数呢?

如果我们看一下模块树,print_user_model函数在crate::models::user_model路径下。所以,为了在非main.rs中使用一个模块,我们将考虑使用能够在模块树中到达被调用模块的路径:

// 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_routecrate::routes::user_route路径下。

我们可以通过使用绝对路径名称crate::routes::health_route::print_health_route(),但是我们也可以使用相对路径super::health_route::print_health_route()。注意,我们使用super来指向父模块(parent scope)。

在模块路径中,super关键字指向父模块

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关键字来将路径绑定到一个新的名字或者别名。

use关键字用来让模块路径变短

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

上面的代码可以重构成:

use crate::models::user_model::print_user_model;

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

如果不使用名称print_user_model,我们也可以给它指定一个别名:

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中的依赖(dependency),对于项目中的所有模块都是可以访问的。我们不用显式导入或者声明什么东西才可以使用某个依赖。

项目中的所有模块都可以访问外部依赖中的模块

举个例子,比如我们在我们项目中增加了rand crate。我们可以在我们的代码中直接使用:

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

我们也可以使用use来让路径更短:

use rand::random;

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

总结

  • 模块系统是显式(explict)的——它和文件系统没有一对一的映射
  • 我们在文件的父模块文件中将文件声明为模块,而不是在文件本身声明自己作为一个模块
  • mod关键字用于声明子模块
  • 我们需要显式地声明函数、结构体等作为public的,从而在别的模块可以使用它们
  • pub关键字让事物变成public的
  • use关键字用于让模块路径变短
  • 我们不用显式地声明第三方模块(引入依赖就可以使用了)

原文链接:https://www.sheshbabu.com/posts/rust-module-system/

显示全文