Skip to main content

Rust 工程化

本章主要包括:

  1. Package(Project) 和 Crates(Target) 的概念
  2. Module(namespace)树的概念
  3. Path 的概念
  4. 如何实现多文件的 Rust 工程
  5. Workspace 的支持(原文 14 章的内容)

使用 Package/Crate/Module 组织源码

在 Rust 的模块系统中, 先来看 packagecrate 的概念.

Crate

crate 是 Rust 编译器在一次识别中的编译单元(compiling unit). 即便是单文件的 crate, Rust 编译器也会将该文件视为一个 crate. crate 可以包含若干 module(名字空间, 用于管理 crate 中代码的作用域和可见性), module 可以分散到多个文件中.

crate 有两种类型:

  • Binary crates: 被打包为二进制可执行程序, 包含 main 函数作为程序入口.
  • Library crates: 被打包为可共享的库(动态或静态链接的均可), 不含 main 函数.

crate root 是这样的一个源文件: Rust 编译器使用它作为起点作为 crate 的根 module.(后面会详细看看 module)

Package

packagecrate 的集合, 可以理解为其它语言工程中 Project 的概念.(crate 可以理解为其它语言中 target 的概念, 即编译目标)

package 中总是含有一个 Cargo.toml 文件, 用于描述如何构建 package 中包含的各个 crate.

Cargo 实际也是一个 package, 其中包含它二进制程序对应的 binary crate, 且其中还包含若干二进制程序依赖的 library crate. 其它的 Rust 程序可以依赖 Cargo library crates 来实现和 Cargo 相同的功能.

一个 package 可以包含任意多个 binary crate, 但只能最多包含一个 library crate. 且一个 package 至少包含一个 crate.

Module

Rust 的模块系统总体规则如下, 即 Rust 编译器眼中是如何看待源文件的:

  • crate root 开始查找模块树: 当编译一个 crate 时, 编译器首先查找 crate root 文件, 比如默认情况下的 src/lib.rssrc/main.rs 文件, 而 crate root 文件隐式形成了一个名为 crate 的模块.

  • 查找子模块 submodules: 在 crate root 文件中, 可以声明 crate 中额外的 module, 比如 mod my_mod;, 编译器会根据如下规则查找 my_mod 对应的源代码:

    • 内嵌(inline)的: 即如果直接在 crate root 中使用 mod my_mod { ... } 定义的模块
    • src/my_mod.rs 文件中的
    • src/my_mod/mod.rs 文件中的
  • 定义子模块 submodules: 在除了 crate root 文件外的其它文件中可以定义子模块, 子模块定义时文件规则和上述查找规则一致. 比如可以再定义一个 my_submod 子模块(在 src/my_submod.rssrc/my_submod/mod.rs 文件中).

  • 通过上述方式, 可以在 crate 中形成如下形式的模块树:

  • 模块的使用: 要使用一个 crate 中定义的模块, 需要通过它的 path 利用 use 去引入, 比如当前 crate 下 mod1 下的 sub_mod1_1 模块有一个 结构体 Helper, 则其 path 为 crate::mod1::sub_mod1_1::Helper.

  • 在模块内部的各种定义上(比如 structenum), 如果没有显式使用 pub 关键字, 则默认情况下都是模块内部私有的. 同样, 如果模块自己没有使用 pub 关键字(pub mod xxx), 则它无法被其它的 crate 使用.

  • 在代码中使用一个模块时, 可以通过 use 关键字进行, 同时可以使用 as 关键字为被引入的模块设置别名.

module 的引用方法

Rust 提供如下两种引用模块的办法:

  • 使用模块的 path 进行"绝对路径"方式引用, 比如 crate::mod1::submod1::MyType
  • 使用 self, super 进行"相对路径"方式引用, 比如:
    • self::submod2::Other
    • super::mod1::Another
    • 如果是 self 的, 可以省略 self 不写(就好像文件系统相对路径不写 ./ 这样) 只要抓住模块树这个概念, 就可以很容易地理解它们.

Rust 中默认情况下本模块中所有 item 都对 parent 模块是私有的, 但 parent 中的 item 对 child 默认不是私有的. Rust 通过这种方式让"隐藏内部实现"成为一种默认行为(除非显式使用 pub 改变可见性)

(同时 parent module 是知道 child module 存在的, 只是 child 中 item 的可见性由 child 控制, 而 module 本身的可见性是针对外部 crate 而言的)

struct 和 enum 成员的可见性

二者规则如下:

  • struct: 如果是 pub 的, 成员仍然默认私有, 除非显式指定 pub
  • enum: 如果是 pub 的, 则成员默认是 pub 的

关于 use 时路径深度的惯用选择

两个规则:

  • 引用类型时使用全路径, 比如 use std::collections::HashMap;
  • 引用函数时保留父 module 路径而不是全路径, 比如 use crate::front_of_house::hosting;, 然后调用 hosting::func1();

这样做没有什么特别的意义, 只是 Rust 这边大家都习惯这样做, 可能这样能够更好避免类型冲突, 以及阅读代码时能轻松发现函数是从哪里拿来的.

as 关键字和 pub use

避免名字冲突的另外一个办法是使用 as 为名字指定别名.

当我们使用 use 引入模块中某个名字使用时, 这个名字对于此模块以外是不可见的, 如果我们想将其暴露给其它模块, 则需要使用 pub use, 这样其它模块就可以只依赖我们这个模块, 而不用再去依赖引入名字的模块重新把名字引入一次了, 且 pub use 的名字对于外部模块而言是作为我们目前模块中的一个名字看的.

使用外部 package

外部 package 提供 library 时肯定都是只有一个 library crate. 使用时通过 包名::绝对路径 即可使用外部包中的公共 API.

另外如果引入同一个子树中多个同级/不同级 module, 还可以简化语法:

// 将 std::io 和 std::io::Write 一起引入进来
use std::io::{self, Write};

使用通配符 * 来引入一个路径下的所有 pub 名字, 比如:

use std::collections::*;

模块树文件结构

Rust 的新风格是使用同名文件作为一个模块, 这样可以避免老风格中导致的出现很多 mod.rs 文件的情况.

新风格比较典型的文件夹组织形式示例如下:

$ tree .
.
├── my
│ ├── inaccessible.rs
│ └── nested.rs
├── my.rs
└── split.rs

可以在Rust Examples 中看到具体的分布. 总而言之就是透过一个代表文件 + 对应目录来实现和之前使用 mod.rs 时一样的结构, 且这样更清晰易懂.