Rust 工程化
本章主要包括:
- Package(Project) 和 Crates(Target) 的概念
- Module(namespace)树的概念
- Path 的概念
- 如何实现多文件的 Rust 工程
- Workspace 的支持(原文 14 章的内容)
使用 Package/Crate/Module 组织源码
在 Rust 的模块系统中, 先来看 package
和 crate
的概念.
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
package
是 crate
的集合, 可以理解为其它语言工程中 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.rs
或src/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
文件中的
- 内嵌(inline)的: 即如果直接在
-
定义子模块
submodules
: 在除了crate root
文件外的其它文件中可以定义子模块, 子模块定义时文件规则和上述查找规则一致. 比如可以再定义一个my_submod
子模块(在src/my_submod.rs
或src/my_submod/mod.rs
文件中). -
通过上述方式, 可以在 crate 中形成如下形式的模块树:
-
模块的使用: 要使用一个
crate
中定义的模块, 需要通过它的path
利用use
去引入, 比如当前 crate 下 mod1 下的 sub_mod1_1 模块有一个 结构体Helper
, 则其 path 为crate::mod1::sub_mod1_1::Helper
. -
在模块内部的各种定义上(比如
struct
或enum
), 如果没有显式使用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
时一样的结构, 且这样更清晰易懂.