什么是项目管理?简单来说,项目管理指的就是对功能的划分和封装。根据功能划分粒度或是封装实现结构的不同,可以派生出很多其他的概念:工作空间、包、库、模块、文件、接口、函数、方法、作用域、公有、私有等等。所以学习项目管理,其实就是学习如何理解并在实际项目中运用定义这些概念。
举个 Java 中的简单例子:
对于一个远程数据库管理项目,根据 MVC 设计模式,可以将其划分为
model
、view
、controller
三个包;而关于
model
可以进一步划分为mapper
、pojo
两个包,分别负责数据源操作映射和数据对象定义;再来看 mapper,一般也会将其划分如下几个部分:定义数据映射相关的对象和方法,将数据库查询语句与映射对象的方法绑定,设置数据库相关的配置等。
……
Rust 中的项目管理无非也是上面这个过程的转述,也就是一些概念的名称变了而已。在 Rust 中,对于功能的划分可以称之为模块系统(the module system),包括如下四个层次:
- 包(Packages): Cargo 的一个功能,它允许你构建、测试和分享 crate。
- Crates :一个模块的树形结构,它形成了库或二进制项目。
- 模块(Modules)和 use: 允许你控制作用域和路径的私有性。
- 路径(path):一个命名例如结构体、函数或模块等项的方式。
以上功能划分均体现在对代码作用域(scope)的控制上。
包和 Crate
crate 分为二进制项或是库两种类型。crate root 是一个源文件,Rust 编译器以它为起始点,并构成你的称为 crate
的根模块。
包(package) 是提供一系列功能的一个或者多个 crate。一个包会包含有一个 Cargo.toml 文件,阐述如何去构建这些 crate。
一个包中至多可以包含一个 crate 库,可以包含任意个 crate 二进制项。包中至少包含一个 crate,无论是库的还是二进制的。
默认情况下,src/main.rs 是一个与包同名的二进制 crate 的 crate 根,*src/lib.rs 是与包同名的库 crate 的 crate 根。
通过将代码文件放在 src/bin 目录下,一个包可以拥有多个二进制 crate:每个 src/bin下的文件都会被编译成一个独立的二进制 crate(crate 名字自定义)。
定义模块来控制作用域和私有域
crate 是一个模块(module)的树形结构,称为模块树(module tree)。模块可以对 crate 中的代码分组,还可以控制项的私有性,项可以包括任意定义的结构、类型、表达式、变量等,比如结构体、枚举、常量、特性、或者函数。
模块使用 mod
关键字来定义,模块中可以包含其它模块和任意项。即通过使用模块,我们可以将相关的定义分组到一起,并指出他们为什么相关。
前面提到了名为 crate
的根模块,也就是说,整个模块树都植根于名为 crate
的隐式模块下。
路径用于引用模块树中的项
可以通过路径来找到一个模块中包含的项的位置。路径有两种形式:
- 绝对路径(absolute path)从 crate 根开始,以 crate 名或者字面值
crate
开头。 - 相对路径(relative path)从当前模块开始,以
self
、super
或当前模块的标识符开头。
通过 ::
标识符来访问模块或是项。
需要注意的是:
- 模块除了作为一种组织代码的方式,它还定义了 Rust 的 私有性边界(privacy boundary):这条界线不允许外部代码了解、调用和依赖被封装的实现细节。所以:需要使用
pub
关键字来暴露模块中某一模块或是项的路径。 - 使模块公有并不使其内容也是公有的,模块上的
pub
关键字只允许其父模块使用它内部的内容,而外部仍然无法使用。 - 对于兄弟模块的使用不需要
pub
。 - 需要引用某一私有模块的内部项时,需要同时将这一模块和项都定义为
pub
。 - 可以使用
super
开头来构建从父模块开始的相对路径。类似于文件系统中的..
访问上一级目录的用法。 - 可以使用
pub
来设计公有的结构体和枚举。不过即便结构体是公有的,但是这个结构体的字段仍然是私有的,需要另外另外定义。而枚举是公有的,则它的所有成员都将变为公有。
使用 use 关键字将名称引入作用域
可以使用 use
关键字将路径一次性引入作用域,然后调用该路径中的项,就如同它们是本地项一样。
在作用域中增加 use
和路径类似于在文件系统中创建软连接(符号连接,symbolic link)。
可以使用 as
关键字来指定别名。还可以使用 pub use
使得路径在外部代码也可直接调用,称为重导出(re-exporting)。
注意在引入使用外部包时,根模块名为包名。
可以使用花括号来嵌套引入路径,对于本身模块的引入,可以用 self
来代替。如,下面两段代码效果是一样的:
// 1
use std::io;
use std::io::Write;
// 2
use std::io::{self, Write};
还可以使用 *
或 glob
引入路径下所有公有项。
将模块分割入不同文件
可以将模块的实现放入不同的源文件中,而在其父模块中使用 pub mod
声明即可,一般将模块的所有子模块放到其相同目录下的同名文件夹中。