Rust 的module system容易让人摸不着头脑。The book
写了很多的内容,但是不适合有一定编码经验的人。
因为里面的内容更多的是强调module的好处,解决了什么问题,并没有对比Rust的module 系统与其他的编程语言的区别。
本文记录Rust module系统让人困惑的三个地方,方便与我有同样困惑的人,也方便我日后参考。
欢迎阅读我其他关于Rust的文章,
Rust: PhantomData,#may_dangle和Drop Check 真真假假
Module System
一个大问题总会由许多小问题组成。module system是为了定义清楚各个小问题的边界。
这样更容易和更方便的管理问题。而大问题的解法,就是把小问题的解法组合起来。
下面是Rust Module System让人困惑的三个地方。
1. project, package, crate, module的区别
project,package, crate, module这些概念感觉相似。
它们的关系是,
package等同于project。cargo new <project> 会创建一个project。
一个package/project可以包含多个 binary crates和0个或者1个library binary。
一个crate可以包含多个module。
当proejct 包含lib.rs,那lib.rs是library crate。bin文件夹包含的文件就是binary crates,可以由cargo new --lib project_name生成。这个library的名字是project的名字。
当project只包含main.rs,那么它就是一个只包含binary crate的project,可以通过命令cargo new --bin project_name生成
可以认为package就是一个project,一个crate就是一个暴露给外界的逻辑单元,一个module就是一个小问题的解法。
比如下面的project 结构
client.rs和server.rs可以直接使用这个library crate。可以认为bin文件夹里面是单独的crate,它们默认依赖了这个library crate。
2. module tree是怎么生成的
project里面的module tree必须显示地指明。
而且一开始的时候,编译器看到的project的module tree仅仅含有main.rs或者lib.rs(下图右边的黄色框框),又叫做crate root。
代码crate::foo 的crate指代的就是这个crate root。
上图左边的目录树,大部分人看到文件系统已经有结构并且组织好,会下意识地认为编译器也会看到这个文件目录结构。但是实际上,编译器看到的project仅仅包含了main.rs。
这一点Rust跟其他语言区别很大。比如在Java里面,你可以直接import 某个目录里面的class。在Rust,如果直接use某个目录的module是编译通不过的。报错如下
error[E0432]: unresolved import `http_server::log`
--> src/bin/main.rs:3:5
|
3 | use http_server::log;
| ^^^^^^^^^^^^^^^^ no `log` in the root
因为你还没有将log添加到crate root里面。也就是要显示地指明module tree的结构。这也就是我们经常在main.rs/lib.rs里面看到许多mod xxx的原因。
比如下面的代码,
https://github.com/Celthi/rsnova/blob/master/src/lib.rs#L20
它们的存在就是为了将project里面的modules 加到这个crate里面。
比如在main.rs 里面看到mod channel,就是将module channel加进crate的module tree来。
这点让我明白了Rust 的”be explicit“原则无处不在。
综上所述,Rust 的module tree的一种样子是,
可能有人会问为什么模块已经存在文件系统里面了,还要显示通过mod foo将模块添加到Rust里面。
可以这么理解,Rust在生成程序的时候,是由crate root去将代码包含进来。
这也就是为什么叫crate root的原因——它是根节点,从这个根节点,一点点地添加模块进来,生成整个package。
3. 父模块和子模块的包含方法
有时候一个folder里面有个mod.rs,有时候没有。这是因为mod foo的作用是告诉编译器找寻foo.rs或者foo/mod.rs,并且将找寻到的文件内容作为module foo的内容。
你可以选择你喜欢的方式使用foo.rs或者foo/mod.rs。
使用文件夹foo存储modules的时候,我们可以创建一个跟文件夹同名的foo.rs来添加文件夹里面的modules,也可以在文件夹foo里面用mod.rs添加对应的modules。
所以,mod foo是将foo模块添加到project的module tree。
use foo;是将一个模块加进当前的scope。
而./foo.rs或者./foo/mod.rs, 或者mod foo {...} 定义模块,它们三者是等效的。
其中./foo.rs可以使用文件夹foo用于存放子模块。./foo/mod.rs已经有文件夹foo,它也可以用于存放子模块。
所以./foo/mod.rs等效于./foo.rs加上./foo/文件夹。
我个人喜欢使用./foo.rs加上文件夹foo来组织子模块。
module的可见性,并不让人困惑,所以本文不提。workspace也比较容易理解,所以略过。
总结
Rust module system提炼为下面三点——
foo.rs或者foo/mod.rs, foo {... } 定义模块foo。
子模块child必须在父模块在声明(mod child),不然它们就不会存在。
子模块child的内容可以mod {... } 显示内联定义在父模块parent.rs里面;可以定义在./child.rs;可以定义在./child/mod.rs。其中当前文件夹是父模块所有的文件夹或者和父模块同名的文件夹。
参考文献
The Edition Guide
Mentally Modelling ModulesRust modules confusion when there is main.rs and lib.rs
本文第一章图片来源于Clear explanation of Rust’s module system。
The confusion around Rust's modules reminds me of the different ways that people...