模块

你刚定义的new方法试图对Ticket的字段值实施一些约束。但这些约束真的被执行了吗?有什么能阻止开发者不通过Ticket::new直接创建Ticket呢?

要实现真正的封装,你需要了解两个新概念:可见性模块。我们先从模块开始讲起。

什么是模块?

在Rust中,模块是一种将相关代码组织在一起的方式,置于一个共同的命名空间下(即模块名)。你已经看过模块的实践了:验证代码正确性的单元测试被定义在一个不同的模块里,名为tests

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    // [...]
}
}

内联模块

上面的tests模块是内联模块的例子:模块声明(mod tests)和模块内容(里面的内容{ ... })紧挨着一起。

模块树

模块可以嵌套,形成树状结构。树的根是crate本身**,即包含所有其他模块的顶级模块。对于库,根模块通常是src/lib.rs(除非位置被自定义过)。

根模块也被称为crate根

根模块可以有子模块,它们反过来也有自己的子模块,以此类推。

外部模块和文件系统

内联模块对小段代码很有用,但随着项目成长,你会想把代码拆分成多个文件。在父模块里,你用mod关键字声明子模块的存在。

#![allow(unused)]
fn main() {
mod dog;
}

Rust的构建工具cargo则负责找到包含模块实现的文件。如果你的模块声明在crate的根目录(如src/lib.rssrc/main.rs),cargo期待文件命名为:

  • src/<module_name>.rs
  • src/<module>/mod.rs

如果你的模块是另一个模块的子模块,文件应命名为:

  • [..]/<parent_module>/<module>.rs
  • [..]/<module>/mod.rs

比如,如果是animals的子模块,那么src/animals/dog.rssrc/og/mod.rs

你的IDE可能在你用mod关键字声明新模块时自动帮你创建这些文件。

项路径和use语句

同一模块里的项可以直接访问,不需要特别语法。直接用它们的名字就行。

#![allow(unused)]
fn main() {
struct Ticket {
    // [...]
}

// 这里不需要限定`Ticket`的任何方式
//因为我们处于同一模块
fn mark_ticket_done(ticket: Ticket) {
    // [...]}
}

但如果你想从不同模块访问实体就不是这样了。你得用指向要访问实体的路径

路径可以用多种方式组合:

  • 从当前crate根开始,比如 crate::module_1::module_2::MyStruct
  • 从父模块开始,比如 super::my_function
  • 从当前模块开始,比如 sub_module::MyStruct

每次引用类型都写全路径可能很繁琐。为了方便,你可以引入use语句来把实体引入作用域。

#![allow(unused)]
fn main() {
// 引入MyStruct`到作用域
use crate::module_1::module_2::MyStruct;

// 现在可以直接引用`MyStruct`
fn a_function(s: MyStruct) {
     // [...]}
}

星号导入

你也可以用一个use语句导入一个模块的所有项。

#![allow(unused)]
fn main() {
use crate::module_1::module_2::*;
}

这称为星号导入
通常不鼓励这样做因为它可能会污染当前命名空间,使得难以理解每个名字来自哪里,并且潜在地引起名称冲突。
尽管如此,在某些情况它还是有用的,比如写单元测试时。你可能注意到多数测试模块以use super::*;开始,引入父模块(被测试的模块)的所有项到作用域。

参考资料

  • 本节练习位于 exercises/03_ticket_v1/03_modules