模块
你刚定义的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.rs
或src/main.rs
),cargo
期待文件命名为:
src/<module_name>.rs
src/<module>/mod.rs
如果你的模块是另一个模块的子模块,文件应命名为:
[..]/<parent_module>/<module>.rs
[..]/<module>/mod.rs
比如,如果是animals
的子模块,那么src/animals/dog.rs
或src/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