对于像我这种初学者来说,Rust 的各种包、模块的逻辑还是需要梳理一下的,想梳理好这部分,需要先弄明白四个对象类型:包、单元包、模块、路径。
包 (package)
一个用于构建、测试并分享单元包的 Cargo 功能。
从概念来看,包中必须要有单元包,在 Cargo 中可以使用如下命令建立一个包,已 modtest 为例:
cargo new modtest
最终会在环境里面建立如下内容:
#cargo new modtest
Created binary (application) `modtest` package
# tree modtest
modtest
├── Cargo.toml
└── src
└── main.rs
1 directory, 2 files
包中默认包含了一个配置文件 Cargo. Toml 和一个 src 目录,src 目录中包含了一个名为 main. Rs 的文件,这个文件就是默认的根单元包,这也意味着一个包中必须要包含至少一个单元包。
单元包(crate)
一个用于生成库或可执行文件的树形模块结构。单元包分为二进制单元包、库单元包,上面介绍的 main. rs 就是一个二进制单元包,执行编译后会生成一个可执行的二进制文件,库单元包一般放在 lib. rs 中。如果想创建一个带有库单元包的包,可以使用如下命令:
cargo new libtest --lib
–lib : 生成一个带有库单元包的包
–bin: 生成一个带有二进制单元包的包(默认)
会生成如下内容:
# cargo new libtest --lib
Created library `libtest` package
# tree ./libtest/
./libtest/
├── Cargo.toml
└── src
└── lib.rs
1 directory, 2 files
对于一个包中单元包还有如下一些限制
- 一个包中必须要包含至少一个单元包
- 单元包有两种类型:二进制单元包(编译后生成二进制程序)、库单元包(编译后生成库)
- 一个包中最多只能有一个库单元包,可以有多个二进制单元包
- 单元包中包含多个模块,Rust编译时使用的入口文件称作单元包的根节点/根模块
模块(mod)
模块的内容会相对比较多,首先模块的诞生个人认为是为了将代码进行切割,对某个代码块进行私密性控制,以此降低 Rust 的源码耦合度。个人感觉跟 C++等面向对象中的类有一些相似度。顺着这个思路走,就需要研讨一下模块的编写和引用了。模块的关键字是 mod,这里我就用《Rust 权威指南》- ISBN: 9787121387067 第七章的例子来说明记录一下。一个餐厅有前厅和后厨两个模块单元,前厅用于服务客户、处理订单、结账等,后厨则主要是做菜和内部管理工作。按照这个逻辑来组织一个单元包,可以将一些动作函数放在对应的模块中。
mod front_of_house{
mod hosting {
fn add_to_waitlist(){}
fn seat_at_table() {}
}
mod serving{
fn take_order(){}
fn serve_order() {}
fn take_payment() {}
}
}
上述代码中通过关键字 mod 定义了一个名字叫做 front_of_house (前厅)的模块,该模块嵌套了两个子模块 hosting、serving,这两个子模块中提供了不同的工作流,从上述实现来看,模块往往也是一个树状形态,与我们常见的文件目录树很类似,可以类比着学习。
完成了模块的编写,需要研究一下如何将这个模块应用在单元包中,以根单元包为例,上述内容写在 src/main. rs 中,并且期望能够运行 hosting 模块中的 add_to_waitlist 函数:
// src/main.rs
mod front_of_house{
mod hosting {
fn add_to_waitlist(){
println!("hosting::add_to_waitlist");
}
fn seat_at_table() {
println!("hosting::seat_at_table")
}
}
mod serving{
fn take_order(){}
fn serve_order() {}
fn take_payment() {}
}
}
fn main() {
println!("Hello, world!");
}
在 main 函数中调用 add_to_waitlist ()函数可以通过两种方式进行调用:绝对路径调用、相对路径调用
绝对路径:使用单元包名或字面量 crate 从根节点开始引用
相对路径:使用 self、super 或内部标识符从当前模块开始引用
绝对路径引入描述如下:
fn main() {
println!("Hello, world!");
// 绝对路径
crate::front_of_house::hosting::add_to_waitlist();
//相对路径
front_of_house::hosting::add_to_waitlist();
self::front_of_house::hosting::add_to_waitlist();
}
按照上面的代码进行编译时会得到一些编译错误的信息
error[E0603]: module `hosting` is private
--> src\main.rs:23:28
|
23 | crate::front_of_house::hosting::add_to_waitlist();
| ^^^^^^^ --------------- function `add_to_waitlist` is not publicly re-exported
| |
| private module
|
从描述来看是在提示模块是私有的,而上述代码中是期望这个模块是公有模块,这就引出模块的私有策略了。
在 Rust 中所有条目(如:函数、方法、结构体、枚举、常量等)默认都是私有的(父子模块之间有个特殊的地方,就是父模块没办法用子模块私有内容,但是反过来则不然)。这里有一个公有的关键字:pub,以上面的为例如果想将 front_of_house::hosting:: add_to_waitlist ()设置为公有内容,不仅要将 front_of_house 设置为公有,每个层次直至最后的条目都需要设置为 pub,原因是与其他语言不同,在 Rust 中包含关系不等同于公私有关系
pub mod front_of_house{
pub mod hosting {
pub fn add_to_waitlist(){
println!("hosting::add_to_waitlist");
}
fn seat_at_table() {
println!("hosting::seat_at_table")
}
}
mod serving{
fn take_order(){}
fn serve_order() {}
fn take_payment() {}
}
}
fn main() {
println!("Hello, world!");
crate::front_of_house::hosting::add_to_waitlist();
front_of_house::hosting::add_to_waitlist();
self::front_of_house::hosting::add_to_waitlist();
}
编译运行后就成功调用相关模块方法了
Hello, world!
hosting::add_to_waitlist
hosting::add_to_waitlist
hosting::add_to_waitlist
还有个地方是我刚开始理解的时候有点难理解的,完成一个单元包编码时不能将所有的 mod 放入 main 文件中,对于拆分成不同的文件 Rust 玩法我还是第一次见过,以上面的代码为例,如果要拆出一个名字叫做 front_of_house.rs 的文件。
Src/front_of_house.rs
pub mod hosting;
建立一个 front_of_house 的目录,在目录中创建新的 mod 文件 hosting.rs
Src/hosting.rs
pub fn add_to_waitlist(){
println!("hosting::add_to_waitlist");
}
fn seat_at_table() {
println!("hosting::seat_at_table")
}
Main 文件中需要增加一行
mod front_of_house;
fn main() {
println!("Hello, world!");
crate::front_of_house::hosting::add_to_waitlist();
front_of_house::hosting::add_to_waitlist();
self::front_of_house::hosting::add_to_waitlist();
}
以上就成功将 mod 进行了文件拆分。个人理解 Rust 是将模块的关联关系通过目录树的形式更可视化的体现了模块树的关联关系。
上述例子中 front_of_house.rs 代表了 front_of_house 这个模块的具体内容,通过建立 front_of_house 名称的文件夹统一存放 front_of_house 模块中的内容。
目录如下:
.
├── front_of_house
│ └── hosting.rs
├── front_of_house.rs
└── main.rs
上面在写整个过程的时候,其实已经把最后一个对象类型描述过了——路径,
路径(path)
一种用于命名条目的方法,这些条目包括结构体、函数和模块等。跟我们理解的路径没有任何区别,就是指向一个对象的标识。这里我没有梳理出来需要描述的,应该是体会到了。
关键字说明
use:该关键字的作用是将某一路径导入对应的作用域,以上面的例子为例,
mod front_of_house;
pub use crate::front_of_house::hosting;
fn main() {
println!("Hello, world!");
hosting::add_to_waitlist();
}
这样在 main 作用域下可以直接使用 hosting 模块中的公有内容了。
super:作用是用于标记父模块,在 front_of_house 中新增一个子模块 serving.rs
Src/front_of_house/serving.rs
pub fn take_order(){
println!("take order!");
}
fn serve_order() {}
fn take_payment() {}
在 src/front_of_house.rs 新增
pub mod hosting;
mod serving;
在 src/front_of_house/hosting.rs 中引用 serving 模块的问题
pub fn add_to_waitlist(){
println!("hosting::add_to_waitlist");
super::serving::take_order(); //新增行,使用了super关键字
}
执行后运行反馈如下:
Hello, world!
hosting::add_to_waitlist
take order!
as:作用就是引用时重命名,避免不同模块中相同的内容冲突。