什么是项目管理?简单来说,项目管理指的就是对功能的划分和封装。根据功能划分粒度或是封装实现结构的不同,可以派生出很多其他的概念:工作空间、包、库、模块、文件、接口、函数、方法、作用域、公有、私有等等。所以学习项目管理,其实就是学习如何理解并在实际项目中运用定义这些概念。

举个 Java 中的简单例子:

对于一个远程数据库管理项目,根据 MVC 设计模式,可以将其划分为 modelviewcontroller 三个包;

而关于 model 可以进一步划分为 mapperpojo 两个包,分别负责数据源操作映射和数据对象定义;

再来看 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)从当前模块开始,以 selfsuper 或当前模块的标识符开头。

通过 :: 标识符来访问模块或是项。

需要注意的是:

  • 模块除了作为一种组织代码的方式,它还定义了 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 声明即可,一般将模块的所有子模块放到其相同目录下的同名文件夹中。