包和模块

包(crate)

Rust 中,crate 是一个独立的可编译单元。具体说来,就是一个或一批文件(如果是一批文件,那么有一个文件是这个 crate 的入口)。它编译后,会对应着生成一个可执行文件或一个库。

执行 cargo new foo,会得到如下目录层级:

  1. foo
  2. ├── Cargo.toml
  3. └── src
  4. └── lib.rs

这里,lib.rs 就是一个 crate(入口),它编译后是一个库。一个工程下可以包含不止一个 crate,本工程只有一个。

执行 cargo new --bin bar,会得到如下目录层级:

  1. bar
  2. ├── Cargo.toml
  3. └── src
  4. └── main.rs

这里,main.rs 就是一个 crate(入口),它编译后是一个可执行文件。

模块(module)

Rust 提供了一个关键字 mod,它可以在一个文件中定义一个模块,或者引用另外一个文件中的模块。

关于模块的一些要点:

  1. 每个 crate 中,默认实现了一个隐式的 根模块(root module)
  2. 模块的命名风格也是 lower_snake_case,跟其它的 Rust 的标识符一样;
  3. 模块可以嵌套;
  4. 模块中可以写任何合法的 Rust 代码;

在文件中定义一个模块

比如,在上述 lib.rs 中,我们写上如下代码:

  1. mod aaa {
  2. const X: i32 = 10;
  3. fn print_aaa() {
  4. println!("{}", 42);
  5. }
  6. }

我们可以继续写如下代码:

  1. mod aaa {
  2. const X: i32 = 10;
  3. fn print_aaa() {
  4. println!("{}", 42);
  5. }
  6. mod BBB {
  7. fn print_bbb() {
  8. println!("{}", 37);
  9. }
  10. }
  11. }

还可以继续写:

  1. mod aaa {
  2. const X: i32 = 10;
  3. fn print_aaa() {
  4. println!("{}", 42);
  5. }
  6. mod bbb {
  7. fn print_bbb() {
  8. println!("{}", 37);
  9. }
  10. }
  11. }
  12. mod ccc {
  13. fn print_ccc() {
  14. println!("{}", 25);
  15. }
  16. }

模块的可见性

我们前面写了一些模块,但实际上,我们写那些模块,目前是没有什么作用的。写模块的目的一是为了分隔逻辑块,二是为了提供适当的函数,或对象,供外部访问。而模块中的内容,默认是私有的,只有模块内部能访问。

为了让外部能使用模块中 item,需要使用 pub 关键字。外部引用的时候,使用 use 关键字。例如:

  1. mod ccc {
  2. pub fn print_ccc() {
  3. println!("{}", 25);
  4. }
  5. }
  6. fn main() {
  7. use ccc::print_ccc;
  8. print_ccc();
  9. // 或者
  10. ccc::print_ccc();
  11. }

规则很简单,一个 item(函数,绑定,Trait 等),前面加了 pub,那么就它变成对外可见(访问,调用)的了。

引用外部文件模块

通常,我们会在单独的文件中写模块内容,然后使用 mod 关键字来加载那个文件作为我们的模块。

比如,我们在 src 下新建了文件 aaa.rs。现在目录结构是下面这样子:

  1. foo
  2. ├── Cargo.toml
  3. └── src
  4. └── aaa.rs
  5. └── main.rs

我们在 aaa.rs 中,写上:

  1. pub fn print_aaa() {
  2. println!("{}", 25);
  3. }

main.rs 中,写上:

  1. mod aaa;
  2. use aaa::print_aaa;
  3. fn main () {
  4. print_aaa();
  5. }

编译后,生成一个可执行文件。

细心的朋友会发现,aaa.rs 中,没有使用 mod xxx {} 这样包裹起来,是因为 mod xxx; 相当于把 xxx.rs 文件用 mod xxx {} 包裹起来了。初学者往往会多加一层,请注意。

多文件模块的层级关系

Rust 的模块支持层级结构,但这种层级结构本身与文件系统目录的层级结构是解耦的。

mod xxx; 这个 xxx 不能包含 :: 号。也即在这个表达形式中,是没法引用多层结构下的模块的。也即,你不可能直接使用 mod a::b::c::d; 的形式来引用 a/b/c/d.rs 这个模块。

那么,Rust 的多层模块遵循如下两条规则:

  1. 优先查找xxx.rs 文件
    1. main.rslib.rsmod.rs中的mod xxx; 默认优先查找同级目录下的 xxx.rs 文件;
    2. 其他文件yyy.rs中的mod xxx;默认优先查找同级目录的yyy目录下的 xxx.rs 文件;
  2. 如果 xxx.rs 不存在,则查找 xxx/mod.rs 文件,即 xxx 目录下的 mod.rs 文件。

上述两种情况,加载成模块后,效果是相同的。Rust 就凭这两条规则,通过迭代使用,结合 pub 关键字,实现了对深层目录下模块的加载;

下面举个例子,现在我们建了一个测试工程,目录结构如下:

  1. src
  2. ├── a
  3. ├── b
  4. ├── c
  5. ├── d.rs
  6. └── mod.rs
  7. └── mod.rs
  8. └── mod.rs
  9. └── main.rs

a/b/c/d.rs 文件内容:

  1. pub fn print_ddd() {
  2. println!("i am ddd.");
  3. }

a/b/c/mod.rs 文件内容:

  1. pub mod d;

a/b/mod.rs 文件内容:

  1. pub mod c;

a/mod.rs 文件内容:

  1. pub mod b;

main.rs 文件内容:

  1. mod a;
  2. use a::b::c::d;
  3. fn main() {
  4. d::print_ddd();
  5. }

输出结果为:i am ddd.

仔细理解本例子,就明白 Rust 的层级结构模块的用法了。

至于为何 Rust 要这样设计,有几下几个原因:

  1. Rust 本身模块的设计是与操作系统文件系统目录解耦的,因为 Rust 本身可用于操作系统的开发;
  2. Rust 中的一个文件内,可包含多个模块,直接将 a::b::c::d 映射到 a/b/c/d.rs 会引起一些歧义;
  3. Rust 一切从安全性、显式化立场出发,要求引用路径中的每一个节点,都是一个有效的模块,比如上例,d 是一个有效的模块的话,那么,要求 c, b, a 分别都是有效的模块,可单独引用。

路径

前面我们提到,一个 crate 是一个独立的可编译单元。它有一个入口文件,这个入口文件是这个 crate(里面可能包含若干个 module)的模块根路径。整个模块的引用,形成一个链,每个模块,都可以用一个精确的路径(比如:a::b::c::d)来表示;

与文件系统概念类似,模块路径也有相对路径和绝对路径的概念。为此,Rust 提供了 selfsuper 两个关键字。

self 在路径中,有两种意思:

  1. use self::xxx 表示,加载当前模块中的 xxx。此时 self 可省略;
  2. use xxx::{self, yyy},表示,加载当前路径下模块 xxx 本身,以及模块 xxx 下的 yyy

super 表示,当前模块路径的上一级路径,可以理解成父模块。

  1. use super::xxx;

表示引用父模块中的 xxx

另外,还有一种特殊的路径形式:

  1. ::xxx::yyy

它表示,引用根路径下的 xxx::yyy,这个根路径,指的是当前 crate 的根路径。

路径中的 * 符号:

  1. use xxx::*;

表示导入 xxx 模块下的所有可见 item(加了 pub 标识的 item)。

Re-exporting

我们可以结合使用 pub use 来实现 Re-exportingRe-exporting 的字面意思就是 重新导出。它的意思是这样的,把深层的 item 导出到上层目录中,使调用的时候,更方便。接口设计中会大量用到这个技术。

还是举上面那个 a::b::c::d 的例子。我们在 main.rs 中,要调用 d,得使用 use a::b::c::d; 来调用。而如果我们修改 a/mod.rs 文件为:
a/mod.rs 文件内容:

  1. pub mod b;
  2. pub use b::c::d;

那么,我们在 main.rs 中,就可以使用 use a::d; 来调用了。从这个例子来看没觉得方便多少。但是如果开发的一个库中有大量的内容,而且是在不同层次的模块中。那么,通过统一导出到一个地方,就能大大方便接口使用者。

加载外部 crate

前面我们讲的,都是在当前 crate 中的技术。真正我们在开发时,会大量用到外部库。外部库是通过

  1. extern crate xxx;

这样来引入的。

注:要使上述引用生效,还必须在 Cargo.tomldependecies 段,加上 xxx="version num" 这种依赖说明,详情见 Cargo 项目管理 这一章。

引入后,就相当于引入了一个符号 xxx,后面可以直接以这个 xxx 为根引用这个 crate 中的 item:

  1. extern crate xxx;
  2. use xxx::yyy::zzz;

引入的时候,可以通过 as 关键字重命名。

  1. extern crate xxx as foo;
  2. use foo::yyy::zzz;