Rust编程学习 - mod (模块)是用于在crate 内部继续进行分层和封装的机制
下面还是用一个完整的示例演示一下build.rs功能如何使用。假设我们现在要把当前项 目最新的commit id记录到可执行程序里面。这种需求就必须使用自动代码生成来完成。首 先新建一个项目with_commit_hash:
cargo new -bin with_commit_hash
然 后 , 到Cargo.toml 里 面 加 上 :
build="build.rs"
当然,对应的,要在项目文件夹下创建 一 个build.rs 的 文 件 。我们希望能在编译过程中生成一份源代码文件,里面记录了一个常量,类似这样:
const CURRENT_COMMIT_ID:&static str ="123456789ABCDEF";
查找当前git 的最新commit id 可以通过命令git rev-parse HEAD 来完成。所以, 我 们 的build.rs 可以这样实现:
use std: :env;
use std: :fs: :File;
use std: :io: :Write;
use std: :path: :Path;
use std: :process: :Command;
fn main() {let out_dir = env: :var ("OUT_DIR").unwrap();let dest_path = Path: :new( & out_dir)·join("commit_id.rs");let mut f = File: :create( & dest_path).unwrap();let commit = Command: :new("git").arg("rev-parse").arg("HEAD").output().expect("Failed to execute git command");let commit = String: :from_utf8(commit.stdout).expect("Invalid utf8 string");let output = format ! (r#"pub const CURRENT_COMMIT_ID:&'static str =" {}";"#, commit);f.write_all(output.as_bytes()).unwrap();
}
输出路径是通过读取OUT_DIR环境变量获得的。利用标准库里面的Command类型, 我们可以调用外部的进程,并获得它的标准输出结果。最后再构造出我们想要的源码字符 串,写入到目标文件中。
生成了这份代码之后,我们怎么使用呢?在main.rs 里面,可以通过宏直接把这部分源 码包含到项目中来:
include ! (concat ! (env ! ("OUT_DIR"), "/commit_id.rs"));
fn main() {println ! ("Current commit id is:{}", CURRENT_COMMIT_ID);
}
这个 include! 宏可以直接把目标文件中的内容在编译阶段复制到当前位置。这样 main 函数就可以访问CURRENT_COMMIT_ID 这个常量了。大家要记得在当前项目使用git 命令新建几个commit。然后编译,执行,可见在可执行程序中包含最新commit id这个任务 就完全自动化起来了。
模块管理
前面我们讲解了如何使用cargo 工具管理crate 。 接下来还要讲解 一 个crate 内部如何管 理模块。可惜的是, Rust 设计组觉得目前的模块系统还有一些瑕疵,准备继续改进,在编写 本书的时候这部分内容正处在热火朝天的讨论过程中。改进的目标是思维模型更简洁、更加 具备一致性、方便各个层次的用户。所以本书在这部分不会强调太多的细节,因为目前一些 看起来比较繁复的细节将来很可能会得到简化。
文件组织
mod (模块)是用于在crate 内部继续进行分层和封装的机制。模块内部又可以包含模 块。Rust 中的模块是一个典型的树形结构。每个crate 会自动产生一个跟当前crate 同 名 的模块,作为这个树形结构的根节点。比如在前面使用cargo 创建多个项目的示例中,项 目 hello_world依赖于项目good_bye, 我们要调用good_bye中的函数,需要写good_ bye::say();, 这是因为say 方法存在于good_bye 这个mod 中。它们组成的树形关系
如下图所示:
在一个crate 内部创建新模块的方式有下面几种。
- 一个文件中创建内嵌模块。直接使用mod 关键字即可,模块内容包含到大括号 内 部 。
mod name{fn items(){} …} - 独立的一个文件就是一个模块。文件名即是模块名。
- 一个文件夹也可以创建一个模块。文件夹内部要有一个mod.rs 文件,这个文件就是这 个模块的入口。
使用哪种方式编写模块取决于当时的场景。如果我们需要创建一个小型子模块,比如单 元测试模块,那么直接写到一个文件内部就非常简单而且直观;如果一个模块内容相对有点 多,那么把它单独写到一个文件内是更容易维护的;如果一个模块的内容太多了,那么把它 放到一个文件夹中就更合理,因为我们可以把真正的内容继续分散到更小的子模块中,而在 mod.rs 中直接重新导出(re-export) 。 这 样mod.rs 的源码就大幅简化,不影响外部的调用者。
可以这样理解:模块是一种更抽象的概念,文件是承载这个概念的实体。但是模块和文 件并不是简单的一一对应关系,用户可以自己维护这个映射关系。
比如,我们有一个crate 内部包含了两个模块, 一个是caller 一个是worker 。我们可以有 几种方案来实现。
方案一:直接把所有代码都写到 lib.rs里面:
//<lib.rs>
mod caller {fn call() {}
}
mod worker {fn work1() {}fn work2() {}fn work3() {}
}
方案二:把这两个模块分到两个不同的文件中,分别叫作caller.rs和 worker.rs 。那么我们的项目就有了三个文件,它们的内容分别是:
//<lib.rs>
mod caller;
mod worker;
//<caller.rs>
fn call() {}
//<worker.rs>
fn work1() {}
fn work2() {}
fn work3() {}
因为lib.rs是这个crate的入口,我们需要在这里声明它的所有子模块,否则caller.rs和 worker.rs 都不会被当成这个项目的源码编译。
方案三:如果worker.rs 这个文件包含的内容太多,我们还可以继续分成几个文件:
/1<lib.rs>
mod caller;
mod worker;
/ / <caller.rs > fn call() {}
//<worker/mod.rs>
mod worker1;
mod worker2;
mod worker3;
//<worker/worker1.rs>
fn work1() {}
//<worker/worker2.rs>
fn work2() {}
//<worker/worker3.rs>
fn work3() {}
这样就把一个模块继续分成了几个小模块。而且worker 模块的拆分其实是不影响 caller 模块的,只要我们在worker 模块中把它子模块内部的东西重新导出(re-export) 就可以了。这 个是可见性控制的内容,下面我们继续介绍可见性控制。
可见性
我们可以给模块内部的元素指定可见性。默认都是私有,除了两种例外情况: 一是用 pub 修饰的trait 内部的关联元素(associated item), 默认是公开的;二是pub enum 内部的成 员默认是公开的。公开和私有的访问权限是这样规定的:
- 如果一个元素是私有的,那么只有本模块内的元素以及它的子模块可以访问;
- 如果一个元素是公开的,那么上一层的模块就有权访问它。
示例如下:
mod top_mod1 {pub fn method1() {}pub mod inner_mod1 {pub fn method2() {}fn method3() {}}mod inner_mod2 {fn method4() {}mod inner_mod3 {fn call_fn_inside() {super: :method4();}}}}
fn call_fn_outside() {: :top_mod1: :method1();: :top_mod1: :inner_mod1: :method2();
}
在这个示例中,top_mod1 外部的函数call_fn_outside(),有权访问method1(),因为它是用pub 修饰的。同样也可以访问method2(), 因为inner_mod1 是pub 的,而且 method2也是pub 的。而inner_mod2 不是pub 的,所以外部的函数是没法访问method4 的。但是call_fn_inside是有权访问 method4的,因为它在method4 所处模块的子模块中。
模块内的元素可以使用pub use 重新导出 (re-export) 。 这也是Rust 模块系统的一个重 要特点。示例如下:
mod top_mod1 {pub use self: :inner_mod1: :method1;mod inner_mod1 {pub use self: :inner_mod2: :method1;mod inner_mod2 {pub fn method1() {}}}}
fn call_fn_outside() {: :top_mod1: :method¹ ();
}
在call_fn_outside 函数中,我们调用了 top_mod1 中的函数method1。 可是我 们注意到,method1 其实不是在top_mod1内部实现的,它只是把它内部inner_mod1 里面 的函数重新导出了而已。pub use 就是起这样的作用,可以把元素当成模块的直接成员公开 出去。我们继续往下看还可以发现,这个函数在inner_mod1里面也只是重新导出的,它 的真正实现是在 inner_mod2里面。
这个机制可以让我们轻松做到接口和实现的分离。我们可以先设计好一个模块的对外 API, 这个固定下来之后,它的具体实现是可以随便改,不影响外部用户的。我们可以把具 体实现写到任何一个子模块中,然后在当前模块重新导出即可。对外部用户来说,这没什么 区别。
