Rust 模块系统

  |   0 评论   |   0 浏览

Rust 提供了一套模块系统来组织和管理代码,包括:模块(module)、Crate(package)和工作空间(workspace)。

包和 Crate

Crate 的英文意思是大木箱,它是一个模块树,并且是编译的基本单元,可以将其编译成可执行程序(executable)或者库(library)。

因此,crate 基本分为两种:二进制Crate(binary crate)和库Crate(library crate)。

(package)是包含一个或者多个crate的文件夹(目录)。

包(package)创建规则

  • 一个包中至多只能包含一个库Crate
  • 包中可以包含任意多个二进制Crate
  • 包中至少包含一个 crate,无论是库的还是二进制的。
  • 包中应该包含一个 Cargo.toml 配置文件,用来说明如何去构建这些 crate

包的简单目录结构

 1.
 2├── Cargo.toml
 3├── src/
 4│   ├── lib.rs
 5│   ├── main.rs
 6│   └── bin/
 7│       ├── named-executable.rs
 8│       ├── another-executable.rs
 9│       └── multi-file-executable/
10│           ├── main.rs
11│           └── some_module.rs
12└── tests/
13    ├── some-integration-tests.rs
14    └── multi-file-test/
15        ├── main.rs
16        └── test_module.rs

src/ 目录存放源代码,tests/ 目录存放测试代码。其中,main.rslib.rs 是两个特殊的 Rust 源文件,lib.rs库Crate 的编译入口,main.rs二进制Crate 的编译入口。另外,src/bin 目录下的文件也会被编译成独立的可执行文件。

包的创建

使用 cargo new 命令创建包。

示例一:创建一个包含 二进制Crate 的包

1$ cargo new my-project  --bin  # --bin 可省略
2     Created binary (application) `my-project` package
3$ ls my-project
4Cargo.toml
5src
6$ ls my-project/src
7main.rs   # 二进制 crate 的编译入口,二进制 crate 与包同名

编译后会生成一个名为my-project或者my-project.exe的可执行文件。

示例二:创建一个包含 库Crate 的包

1$ cargo new my-project  --lib
2     Created library (application) `my-project` package
3$ ls my-project
4Cargo.toml
5src
6$ ls my-project/src
7lib.rs   # 库 crate 的编译入口,库 crate 与包同名

编译之后会生成一个名为 libmy-project.rlib 的 Rust 库文件。这是一种默认的格式,当然也可以生成其他格式的库文件。

在 Linux 系统中使用下面的命令查看可以编译成哪些文件类型(格式):

1$ rustc --help|grep crate-type
2        --crate-type [bin|lib|rlib|dylib|cdylib|staticlib|proc-macro]

示例三:创建一个包含 二进制Crate库Crate 的包

Rust没有提供这样的命令,cargo new中的--bin--lib选项不能同时使用。

1$ cargo new my-object --bin --lib
2error: can't specify both lib and binary outputs

其实很简单,只需在示例一的基础上,在src目录下添加一个lib.rs文件即可。

一般情况下,我们将与程序运行相关的代码放在 main.rs 文件,其他真正的任务逻辑放在 lib.rs 文件中。

模块(Moduls)

模块是对 crate 中的代码进行分组的最简单直接的方法。

除了能够提高代码可读性和重用性,还可以控制模块中内容对外的访问权限(公有或私有)。另外,模块也是可以嵌套的。

在 Rust 中每个源文件(.rs 后缀)都是一个模块,但不是所有的模块都有自己专属的源文件。

模块定义

使用mod关键字定义模块。

示例一:在 src/lib.rs 中定义三个模块

 1// 其中 hosting 和 serving 是 front_of_house 的子模块。
 2mod front_of_house {
 3    mod hosting {
 4        fn add_to_waitlist() {}
 5        fn seat_at_table() {}
 6    }
 7
 8    mod serving {
 9        fn take_order() {}
10        fn server_order() {}
11        fn take_payment() {}
12    }
13}

在一个文件中可以定义多个模块,当模块变大变多时,也可以将模块放到单独的文件中。

示例二:在文件中定义单个模块

1// src/front_of_house.rs 文件中定义了一个单独的模块
2pub mod hosting {
3    pub fn add_to_waitlist() {}
4}

模块引用

想要引用其他模块中的代码,需要定位这个模块。与文件系统类似,Rust 提供了两种路径形式:

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

示例一:在 src/lib.rs 中定义模块并引用

 1// 在 Rust 中模块、函数等默认都是私有的,只有使用 pub 关键字声明为公共的才能被引用。
 2// 父模块中的项不能使用子模块中的私有项,但是子模块中的项可以使用他们父模块中的项。
 3mod front_of_house {
 4    pub mod hosting {
 5        pub fn add_to_waitlist() {}
 6    }
 7}
 8
 9pub fn eat_at_restaurant() {
10    // 绝对路径
11    // `crate` 代表当前 Crate 中的根,类似于文件系统中的根目录
12    crate::front_of_house::hosting::add_to_waitlist();
13
14    // 相对路径
15    // eat_at_restaurant 函数与 front_of_house 模块在同一个 Crate 的同一层级,所以从 front_of_house 开始
16    front_of_house::hosting::add_to_waitlist();
17}

示例二:使用 superself 消除歧义

 1fn function() {
 2    println!("called `function()`");
 3}
 4
 5mod cool {
 6    pub fn function() {
 7        println!("called `cool::function()`");
 8    }
 9}
10
11mod my {
12    fn function() {
13        println!("called `my::function()`");
14    }
15  
16    mod cool {
17        pub fn function() {
18            println!("called `my:🆒:function()`");
19        }
20    }
21  
22    pub fn indirect_call() {
23        print!("called `my::indirect_call()`, that\n> ");
24      
25        // `self` 关键字表示当前的模块 -- `my`。
26        // 调用 `self::function()` 和直接调用 `function()` 都得到相同的结果,因为他们表示相同的函数。
27        self::function();
28        function();
29      
30        // 也可以使用 `self` 来访问 `my` 内部的另一个模块。
31        self::cool::function();
32      
33        // `super` 关键字表示父作用域(在 `my` 模块外面)。
34        super::function();
35      
36        // 绑定 *crate* 作用域(最外层)内的 `cool::function` 。
37        {
38            use crate::cool::function as root_function;
39            root_function();
40        }
41    }
42}

use 关键字

使用绝对路径或者相对路径调用其他模块中的函数虽然简单直接,但是比较麻烦。因此,Rust 提供了一种简化方式。与 Java 中导入包类似,在 Rust 中使用的是 use 关键字将模块引入当前作用域。

示例一:使用 use 将模块引入作用域

 1mod front_of_house {
 2    pub mod hosting {
 3        pub fn add_to_waitlist() {}
 4    }
 5}
 6// 使用 use 指定模块路径。
 7use crate::front_of_house::hosting;
 8
 9// hosting 模块在当前作用域就是有效的,可以直接使用。
10// 前提是 hosting 模块中的函数是公有的。
11pub fn eat_at_restaurant() {
12    hosting::add_to_waitlist();
13    hosting::add_to_waitlist();
14    hosting::add_to_waitlist();
15}

示例二:使用 as 关键字重命名引入作用域的类型

 1use std::fmt::Result;
 2// 引入其他模块时,可以把引入的结构体、函数等重命名
 3use std::io::Result as IoResult;
 4
 5fn function1() -> Result {
 6    // --snip--
 7}
 8
 9fn function2() -> IoResult<()> {
10    // --snip--
11}

示例三:使用外部包

首先要在 Cargo.toml 中配置要依赖的外部包的名称和版本等信息:

1[dependencies]
2rand = "0.5.5"  # Cargo 会自动去下载这个依赖

然后就可以在代码中使用 use 关键字引入这个 Crate 了:

1use rand::Rng;
2
3fn main() {
4    let secret_number = rand::thread_rng().gen_range(1, 101);
5}

标准库(std)也是外部 crate,因为标准库随 Rust 语言一同发行,无需配置 Cargo.toml 来引入 std,不过需要通过 use 将标准库中定义的项引入项目包的作用域中来引用它们,比如我们使用的 HashMap

1use std::collections::HashMap;

文件分层

当模块比较多时,我们可以讲模块分到不同的文件中,方便管理。

示例:重构之前的示例

1.
2├── Cargo.toml
3├── src/
4│   ├── lib.rs
5│   ├── main.rs
6│   ├── front_of_house.rs
7│   └── front_of_house
8│       └── hosting.rs

按照上面的目录结构创建一个 Rust 项目:

 1PS D:\Github> cargo new restaurant
 2     Created binary (application) `restaurant` package
 3PS D:\Github> cd .\restaurant\src\
 4PS D:\Github\restaurant\src> New-Item lib.rs
 5
 6    目录: D:\Github\restaurant\src
 7Mode                LastWriteTime         Length Name
 8----                -------------         ------ ----
 9-a----        2021/1/24     17:02              0 lib.rs
10
11PS D:\Github\restaurant\src> New-Item front_of_house.rs
12
13    目录: D:\Github\restaurant\src
14Mode                LastWriteTime         Length Name
15----                -------------         ------ ----
16-a----        2021/1/24     17:02              0 front_of_house.rs
17
18PS D:\Github\restaurant\src> mkdir front_of_house
19
20    目录: D:\Github\restaurant\src
21Mode                LastWriteTime         Length Name
22----                -------------         ------ ----
23d-----        2021/1/24     17:02                front_of_house
24
25PS D:\Github\restaurant\src> New-Item .\front_of_house\hosting.rs
26
27    目录: D:\Github\restaurant\src\front_of_house
28Mode                LastWriteTime         Length Name
29----                -------------         ------ ----
30-a----        2021/1/24     17:03              0 hosting.rs
31
32PS D:\Github\restaurant\src>

src/front_of_house/hosting.rs 文件中的内容:

1pub fn add_to_waitlist() {}

src/front_of_house.rs 文件中的内容:

1// 查找名为 `hosting.rs` 的文件,
2// 并将该文件的内容放到一个名为 `hosting` 的模块里面。
3pub mod hosting;

lib.rs 文件中的内容:

 1// 查找名为 `front_of_house.rs` 的文件,
 2// 并将该文件的内容放到一个名为 `front_of_house` 的模块里面。
 3mod front_of_house;
 4
 5// 使用 `pub use` 重导出(Re-exports)
 6// 重导出后,不仅当前模块可以使用 `hosting` 模块,在当前模块之外也可以使用
 7pub use crate::front_of_house::hosting;
 8
 9pub fn eat_at_restaurant() {
10    hosting::add_to_waitlist();
11    hosting::add_to_waitlist();
12    hosting::add_to_waitlist();
13}

main.rs 文件的内容:

 1// 引入 restaurant 库Crate 中的代码
 2// `restaurant` 是 库Crate 的名称,通常与包名相同
 3use restaurant::*;
 4
 5fn main() {
 6    eat_at_restaurant();
 7
 8    // 此处可以调用 `hosting` 模块中的函数
 9    // 是因为在 `lib.rs` 中对`hosting` 模块进行了重新导出
10    hosting::add_to_waitlist();
11}

工作空间

随着项目开发的深入,库Crate 持续增大,有必要将其进一步拆分成多个库Crate。对于这种情况,Cargo提供了一个叫 工作空间(workspaces)的功能,它可以帮助我们管理多个相关的协同开发的包。

创建工作空间

  1. 新建一个工作空间目录。
    示例
1$ mkdir add
2$ cd add
  1. 在工作空间目录中创建Cargo.toml文件,并在文件中配置members (Crate 的名称)。

示例

1[workspace]
2members = [
3    "adder","add-one","add-two",
4]
  1. 根据配置创建包

示例

1$ cd add
2$ cargo new adder  # 新建一个包含二进制Crate的包
3$ cargo new add-one --lib # 新建一个包含库Crate的包
4$ cargo new add-two --lib # 新建另一个包含库Crate的包
  1. 配置依赖

示例
假如adder依赖了add-one,需要在adderCargo.toml文件中进行相应的配置。

1[dependencies]
2add-one = { path = "../add-one" }

配置之后就可以在 adder/src/main.rs 中引用 add-one 库了。

1use add_one;
2fn main() {
3    let num = 10;
4    println!("Hello, world! {} plus one is {}!", num, add_one::add_one(num));
5}

其中 add_one::add_oneadd-one/src/lib.rs 中定义的函数:

1pub fn add_one(x: i32) -> i32 {
2    x + 1
3}
  1. 构建和运行

在工作空间目录中运行 cargo build 来构建工作空间。该命令会编译所有的 Crate,并将编译好的文件统一放到当前目录下/target/debug目录。

如果工作空间中仅有一个 二进制Crate,直接在工作空间目录中使用cargo run 命令来运行。

如果工作空间中有多个 二进制Crate,需要通过 -p 参数指定包名称来运行工作空间中的包。比如:

1$ cargo run -p adder
2    Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
3     Running `target/debug/adder`
4Hello, world! 10 plus one is 11!

相关资料

Rust Programming Language

Rust by Example

Crates and source files

Modules

Cargo Workspaces