在开发一个复杂的应用程序的时候,我们需要把各个功能拆分、封装到不同的文件,在需要的时候引用该文件。没人会写一个几万行代码的文件,这样在可读性、复用性和维护性上都很差,几乎所有的编程语言都有自己的模块组织方式,比如Java中的包、C#中的程序集等。Rust也不例外, 但是Rust的模块文档是从顶部设计开始写的,很多概念,有些复杂,我记得刚接触 Rust 时模块让我痛苦挣扎,所以我尝试用一种我认为说得通的方式解释它。
什么是 crate
一个crate通常来说是一个项目. Rust创建项目(crate)通常使用包管理器cargo, cargo new hello_cargo, 这样就会新建一个文件夹, 并且文件夹内有一个 Cargo.toml 配置文件,这个文件用于声明依赖,入口,构建选项等项目元数据。 每个 crate 可以独立地在 https://crates.io/ 上发表。
假设我们要创建一个二进制(可执行)项目:
cargo new –bin
项目入口为 src/main.rs
对于二进制项目,src/main.rs 是项目主模块的常用路径。
默认情况下,我们的可执行项目的 src/main.rs 如下:
fn main() {
println!("Hello world!");
}
我们可以通过 cargo run 构建和运行这个项目,若只想构建项目,则运行 cargo build
构建一个 crate 的时候,cargo 下载并编译所有所需依赖,默认情况下把临时文件和最终生成文件放入 ./target/ 目录下。 cargo 既是包管理器又是构建系统。
crate 依赖
让我们向刚才创建的 crate 添加 time 依赖来看看命名空间是怎么工作的。如果你的 Cargo.toml,还没有 [dependencies] 部分,添加它,然后列出您要使用的包名称和版本。这个例子增加了一个 time 箱 (crate) 依赖:
[dependencies]
time = "0.1.12"
如果我们还想添加一个 regex crate子依赖,我们不需要为每个箱子都添加 [dependencies]。下面就是你的 Cargo.toml 文件整体,看起来像依赖于 time 和 regex 箱:
[package]
name = "my-rust"
version = "0.1.0"
authors = ["sulin <723719990@qq.com>"]
edition = "2018"
[dependencies]
time = "0.1.12"
regex = "0.1.41"
你现在可以使用 regex crate了.
现在让我们在 src/main.rs 里使用 regex, src/main.rs 如下:
fn main() {
let re = regex::Regex::new(r"^\d{4}-\d{2}-\d{2}$").unwrap();
println!("Did our date match? {}", re.is_match("2020-01-01"));
}
请注意:
我们不需要使用 use 指令来使用 regex - 它在项目下的文件全局可用,因为它在 Cargo.toml 中被声明为依赖(rust 2018之前的版本则不是这样)
我们完全没必要使用 mod (稍后讲述)
为了明白这篇博客的余下部分,你需要明白 rust 模块仅仅是命名空间 - 他们让你把相关符号组合在一起并保证可见性规则。
我们的 crate 有一个主模块(我们现在所在),它的源在 src/main.rs
regex crate 也有一个入口。因为他是一个库,默认情况下其主入口为 src/lib.rs
在我们主模块范围,我们可以在主模块通过依赖名称使用依赖
总之,我们现在只处理两个模块:我们项目主入口还有 regex 的入口。
use 指令
如果我们不喜欢一直这样写 regex::Regex::new(),我们可以把 regex::Regex 注入主模块范围。
use regex::Regex;
// 我们可以通过 `Regex::new()`
fn main() {
let re = Regex::new(r"^\d{4}-\d{2}-\d{2}$").unwrap();
println!("Did our date match? {}", re.is_match("2020-01-01"));
}
如果希望将一个路径下所有公有项引入作用域,可以指定路径后跟 * 通配符. 使用通配符时请多加小心!会使得我们难以推导作用域中有什么名称和它们是在何处定义的。所以不推荐使用.
模块不需要在分开的文件里
模块让我们可以将一个crate中的代码进行分组,以提高可读性与重用性。模块还可以控制项的私有性,即项是可以被外部代码使用的(public),还是作为一个内部实现的内容,不能被外部代码使用(private)。模块是一个让你组合相关符号的语言结构.
你不需要把他们放在不同的文件下.
下面我们以餐饮业为例,餐馆中会有一些地方被称之为 前台(front of house),还有另外一些地方被称之为 后台(back of house)。前台是招待顾客的地方,在这里,店主可以为顾客安排座位,服务员接受顾客下单和付款,调酒师会制作饮品。后台则是由厨师工作的厨房,洗碗工的工作地点,以及经理做行政工作的地方组成。
我们可以将函数放置到嵌套的模块中,来使我们的 crate 结构与实际的餐厅结构相同。
让我们修改下 src/main.rs 来证明这个观点:
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
pub fn seat_at_table() {}
}
pub mod serving {
pub fn take_order() {}
pub fn server_order() {}
pub fn take_payment() {}
}
}
fn main() {
crate::front_of_house::hosting::add_to_waitlist();
crate::front_of_house::hosting::seat_at_table();
crate::front_of_house::serving::take_order();
crate::front_of_house::serving::server_order();
crate::front_of_house::serving::take_payment();
}
从范围角度,我们项目结构如下:
crate 主模块
└── front_of_house
├── hosting
│ ├── add_to_waitlist
│ └── seat_at_table
└── serving
├── take_order
├── serve_order
└── take_payment
从文件角度, 主模块和 front_of_house 模块都在同一个文件 src/main.rs 下。
模块可以在可分开的文件中
现在, 如果我们如下修改项目:
src/front_of_house.rs
pub mod hosting {
pub fn add_to_waitlist() {}
pub fn seat_at_table() {}
}
pub mod serving {
pub fn take_order() {}
pub fn server_order() {}
pub fn take_payment() {}
}
src/main.rs
fn main() {
crate::front_of_house::hosting::add_to_waitlist();
crate::front_of_house::hosting::seat_at_table();
crate::front_of_house::serving::take_order();
crate::front_of_house::serving::server_order();
crate::front_of_house::serving::take_payment();
}
然而这行不通.
Compiling my-rust v0.1.0 (/Users/sulin/project/rust/my-rust)
error[E0433]: failed to resolve: maybe a missing crate `front_of_house`?
--> src/main.rs:2:12
|
2 | crate::front_of_house::hosting::add_to_waitlist();
| ^^^^^^^^^^^^^^ maybe a missing crate `front_of_house`?
虽然 src/main.rs 和 src/lib.rs(二进制和库项目)会被 cargo 自动识别为程序入口,其他文件则需要在文件中明确声明。
我们的错误在于仅仅创建了 src/front_of_house.rs 文件,希望 cargo 会在构建时找到它,但事实上并不是这样的。cargo 甚至不会解析它。 cargo check 命令也不会报错,因为 src/front_of_house.rs 现在还不是 crate 源文件的一部分。
为了改正这个错误,可以如下修改 src/main.rs(因为它是项目入口,这是 cargo 已知的):
mod front_of_house {
include!("front_of_house.rs");
}
// 注意: 这不是符合 rust 风格的写法,仅作 mod 学习用
fn main() {
crate::front_of_house::hosting::add_to_waitlist();
crate::front_of_house::hosting::seat_at_table();
crate::front_of_house::serving::take_order();
crate::front_of_house::serving::server_order();
crate::front_of_house::serving::take_payment();
}
现在 crate 可以编译和运行了,因为:
我们定义了一个名为 front_of_house 的模块
我们告诉编译器复制/粘贴其他文件(front_of_house.rs)到模块代码块中
参考 include! 文档
但这不是通常导入模块的方式。按照惯例,如果使用不跟随代码块的 mod 指令,效果上述一样。
所以也可以这样写:
mod front_of_house;
fn main() {
crate::front_of_house::hosting::add_to_waitlist();
crate::front_of_house::hosting::seat_at_table();
crate::front_of_house::serving::take_order();
crate::front_of_house::serving::server_order();
crate::front_of_house::serving::take_payment();
}
就是这么简单。但容易混淆之处在于,根据 mod 之后是否有代码块,它可以内联定义模块,或者导入其他文件。
这也解释了为什么在 src/front_of_house.rs 里不用再定义另一个 mod math {}。因为 src/front_of_house.rs 已经在 src/main.rs 中导入,它已经说 src/front_of_house.rs 的代码存在于一个名为 front_of_house 的模块中。
那 use 呢
现在我们几乎了解了 mod,那 use 呢?
use 的唯一目的是将符号带入命名空间,让符号使用更加简短。
特别是,use 永远不会告诉编译器去编译 mod 导入文件之外的其他文件。
在 main.rs/front_of_house.rs 例子中,在 src/main.rs 写下如下语句时:
mod front_of_house;
我们在主模块导入一个名为 front_of_house 模块,这个模块导出 其他 模块.
从范围角度,结构如下:
crate 主模块(我们在这儿)
└── front_of_house 模块
├── hosting 模块
│ ├── add_to_waitlist 函数
│ └── seat_at_table 函数
└── serving 模块
├── take_order 函数
├── serve_order 函数
└── take_payment 函数
这就是为什么我们要使用 add_to_waitlist 函数时要这样引用 front_of_house::hosting::add_to_waitlist,即从主模块到 add_to_waitlist 函数的正确路径。
请注意,如果我们从另一个模块调用 add_to_waitlist,那么 front_of_house::hosting::add_to_waitlist 可能不是有效路径。 然而,add_to_waitlist 有一个更长的添加路径,即 crate::front_of_house::hosting::add_to_waitlist - 它在我们的 crate 中的任何位置都有效(只要 front_of_house 模块保持原样)。
所以,如果我们不想每次都使用 front_of_house::hosting:: 前缀调用 add_to_waitlist 可以用 use 指令:
mod front_of_house;
use front_of_house::hosting;
use front_of_house::serving;
fn main() {
hosting::add_to_waitlist();
hosting::seat_at_table();
serving::take_order();
serving::server_order();
serving::take_payment();
}
那 mod.rs 又是什么呢?
好吧,我说谎了 - 我们还没完全了解 mod。
目前,crate 有一个漂亮又扁平的文件结构:
src/
main.rs
front_of_house.rs
其实这不是很有道理的,因为 front_of_house 里有很多小模块,我们可以这样改变它的结构, 让代码结构更清晰:
src/
main.rs
front_of_house/
mod.rs
就命名空间/范围而言,两种结构都是等价的。我们的新 src/front_of_house/mod.rs 与src/front_of_house.rs具有完全相同的内容, 并且我们的 src/main.rs 完全不变。
事实上,如果我们定义了 front_of_house 模块的子模块, front_of_house/mod.rs 结构更加易于理解。
我们现在的文件结构如下:
src/
main.rs
front_of_house/
mod.rs
serving.rs (新文件!)
hosting.rs (也是新文件!)
概念上而言,命名空间树如下:
crate (src/main.rs)
`front_of_house` 模块 (src/front_of_house/mod.rs)
`serving` 模块 (src/front_of_house/serving.rs)
`hosting` 模块 (src/front_of_house/hosting.rs)
我们的 src/main.rs 不需要做很大改动 - front_of_house 仍在相同位置。我们只是让它使用 serving 和 hosting:
// 保证 front_of_house 在 `./front_of_house.rs` 或 `./front_of_house/mod.rs` 中定义
mod front_of_house;
// 将两个符号带入范围,在 `front_of_house` 模块中保证都已导出
use front_of_house::{hosting, serving};
fn main() {
hosting::add_to_waitlist();
hosting::seat_at_table();
serving::take_order();
serving::server_order();
serving::take_payment();
}
我们的 src/front_of_house/hosting.rs 正如我们在 front_of_house.rs 模块做的一样:定义一个函数,并用 pub 将其导出。
pub fn add_to_waitlist() {}
pub fn seat_at_table() {}
类似地,src/front_of_house/serving.rs 文件如下:
pub fn take_order() {}
pub fn server_order() {}
pub fn take_payment() {}
现在来看 src/front_of_house/mod.rs。我们知道 cargo 知道 front_of_house 这个模块存在, 因为 src/main.rs 中的 mod front_of_house; 语句已将其导入。 但我们需要让 cargo 也知道 hosting 和 serving 模块。
所以我们需要在 src/front_of_house/mod.rs 添加如下语句;
pub mod hosting;
pub mod serving;
现在 cargo 知晓所有源文件。
crate 能编译成功了.
这样改变后,从 src/front_of_house/mod.rs 角度看,模块结构如下:
`front_of_house` 模块
`hosting` 模块(公开)
`add_to_waitlist` 函数(公开)
`seat_at_table` 函数(公开)
`serving` 模块(公开)
`take_order` 函数(公开)
`server_order` 函数(公开)
`take_payment` 函数(公开)
结论
希望这些能澄清 rust 的模块和文件, 如果有任何疑问, 可以在社区进行交流.