将代码拆分为不同的模块并使用不同的文件来管理它们
一个包 (package) 可以拥有:
- 多个二进制单元包 (crate);
- 一个可选的库单元包 (crate);
将部分代码拆分到独立的单元包 crate 中,并将它作为外部依赖进行引用。
模块系统:
- 包:用于构建、测试并分享包的 Cargo 功能;
- 单元包:用于生成代码库或可执行文件的树状模块结构;
- 模块和 use 关键字:用于控制文件结构、作用域以及路径的私有性;
- 路径:用于命名条目的方法,这些条目包括结构体、函数、模块等;
包与单元包
单元包是 Rust 编译器可以单次处理的最小代码集合。单元包可以包含一系列模块,单个文件也可作为一个单元包,模块可以被定义在不同的文件内,并在编译时被整合至单元包内。
单元包以两种形式存在,二进制单元包与库单元包。二进制单元包可被编译成可执行文件,每个二进制单元包必须包含一个名为 main 的函数,它作为入口函数定义了程序执行的行为。
库单元包内没有 main 函数,也不会被编译为可执行文件,被用来定义一些可以在多个项目中共享的功能。
Rust 编译时所使用的入口文件称为这个单元包的根节点,同时也是单元包的根模块。
包由一个或多个提供相关功能的单元包集合而成,Cargo.toml 描述了如何构建这些单元包信息。Cargo 本身也是一个包,它包含的二进制单元包提供了对应的命令行工具。
在使用 cargo new 一个新工程项目时 (个人习惯于添加 –vcs none 不启用版本控制系统,默认 Git),实际上就创建了一个包,目录下有 Cargo.toml 文件,Cargo 会默认将 src 目录下的 main.rs 文件一个二进制单元包的根节点而无须指定,若是库单元包,则将 src 目录下的 lib.rs 文件视为库单元包的根节点,Cargo 在构建二进制程序和库时,会将这些单元包的根节点文件作为参数传递给 rustc。
可以在 src/bin 目录下添加源文件来创建出更多的二进制单元包。
my_project/
├── Cargo.toml
├── src/
│ ├── lib.rs # 库代码
│ └── bin/ # 可执行文件目录
│ ├── main1.rs # 生成可执行文件 main1
│ ├── main2.rs # 生成可执行文件 main2
│ └── tools/ # 可以嵌套子目录
│ └── util.rs # 生成可执行文件 util
└── target/
└── debug/
├── main1
├── main2
└── util
模块
以上说明了下,当开始编译时,是从单元包的根节点文件开始编译 (即 src/main.rs 或 src/lib.rs),若在 main.rs 中声明一个 garden 模块时 (使用 mod garden),编译器会在如下路径中搜索模块代码:
- 内嵌代码,在 mod garden 后面使用花括号替代分号来创建的代码块;
- src/garden.rs;
- src/garden/mod.rs (老式用法);
子模块,假设 garden 有子模块 vegetables,以下是在 src/main.rs 中一步到位,搜索模块代码是在内嵌里搜索
mod garden {
mod vegetables {
//...
}
}
若是在 src/main.rs 中定义如下,编译器则会在 src/garden.rs 下搜索
若是在 src/main.rs 中定义如下,编译器则会在 src/garden.rs 下搜索
pub mod garden;
src/garden.rs 中定义 vegetables 子模块,编译器则会在 src/garden/vegetables.rs 中搜索模块代码
pub mod vegetables;
src/garden/vegetables.rs 中内容:
#[derive(Debug)]
pub struct Asparagus {}
模块内的代码相对于其父模块默认是私有的 (包含了内部实现的细节,无法被外部使用)。为了使一个模块公共化,需加上 pub 关键字,接下来就可以用快捷路径方式来引用模块代码 (使用 use),在 src/main.rs 中
// main.rs 或 lib.rs 被称为包的根节点
// 这两个文件内容各自组成了一个名为 crate 的模块
// 位于单元包模块结构的根部,是隐藏的
use crate::garden::vegetables::Asparagus;
// 也可使用 use garden::vegetables::Asparagus;
// 告诉编译器嵌入它在 src/garden.rs 文件找到的内容
pub mod garden;
fn main() {
let plant = Asparagus {};
println!("I'm growing {:?}!", plant);
}
库单元包创建
使用 cargo new [项目名] –vcs none –lib 命令来创建,src 目录下就没有 main.rs 了,而是 lib.rs。
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 关键字定义一个模块,后面跟着模块名,花括号中为模块体,通过模块,可以将相关的定义进行分组管理,若要使用它们,就需要指出路径。
路径有两种形式,绝对路径与相对路径。
从单元包根节点出发,即绝对路径,对于外部单元包而言,绝对路径以这个单元包的名称开始,而对于当前单元包中的代码而言,绝对路径以 crate 开始。
相对路径使用 self、super 或内部标识符从当前模块开始。
绝对路径与相对路径都由一个或多个标识符组成,标识符之间使用双冒号 (::) 分隔。
如何调用 add_to_waitlist() 函数,或者说 add_to_waitlist() 的路径是什么?
mod front_of_house {
mod hosting {
fn add_to_waitlist() {}
}
}
fn eat_at_restaurant() {
// 绝对路径方式
crate::front_of_house::hosting::add_to_waitlist();
// 相对路径方式
front_of_house::hosting::add_to_waitlist();
}
当 cargo check 时,以上代码编译检查不通过,原因是 hosting 模块为私有,包括 add_to_waitlist() 也是私有,若想通过编译检查,只需简单修改下
mod front_of_house {
pub mod hosting {
// 将模块标记为公开并不意味着内部条目也是公开
// 模块前面 pub 仅仅意味祖先模块拥有了指向该模块的权限
pub fn add_to_waitlist() {}
}
}
// 因为是作为库单元包,要供其它调用,所以这里也用 pub
pub fn eat_at_restaurant() {
// 绝对路径方式
crate::front_of_house::hosting::add_to_waitlist();
// 相对路径方式
front_of_house::hosting::add_to_waitlist();
}
最后在项目中到底是使用绝对路径还是相对路径,取决于你是更有可能将条目定义代码与使用该条目的代码分开移动,还是将它们一起移动。大部分的 Rust 开发者会更倾向于使用绝对路径。
处于父模块中的条目无法使用子模块中的私有条目,但子模块中的条目可以使用其祖先模块中的条目,这种设计的目的是希望隐藏内部实现的细节,能够明确地知道修改哪些内部实现不会破坏外部代码,但你也可以用 pub 将某些条目标记为公共的,从而使子模块中的这些部分暴露到祖先模块中。
将模块标记为公开并不意味着内部条目也是公开,模块前面 pub 仅仅意味祖先模块拥有了指向该模块的权限,单独将模块声明为公共的并不会产生多大的作用,因为模块本身只被视作一个容器,我们需要深入到模块内部,选择将其中的一个或多个条目同样声明为公共的。
super 构造相对路径
示例代码
fn deliver_order() {}
mod back_of_order {
fn fix_incorrect_order() {
cook_order();
super::deliver_order();
}
fn cook_order() {}
}
super 关键跳转至 back_of_order 的父模块,从它开始,可以找到 deliver_order。
应用于结构体与枚举
在结构体定义前使用 pub 时,结构体本身就成为公共结构体,但它的字段依旧保持私有状态,如果要将某个字段公开,需要加上 pub
mod back_of_house {
pub struct Breakfast {
pub toast: String,
seasonal_fruit: String,
}
impl Breakfast {
// 提供一个公共的关联函数来构建 Breakfast 实例
pub fn summer(toast: &str) -> Breakfast {
Breakfast {
toast: String::from(toast),
seasonal_fruit: String::from("peaches"),
}
}
}
}
pub fn eat_at_restaurant() {
let mut meal = back_of_house::Breakfast::summer("Rye");
meal.toast = String::from("Wheat");
println!("I'd like {} toast please", meal.toast);
}
在以上代码中,Breakfast 结构体中 toast 是公开的,seasonal_fruit 是私有的,若在 eat_at_restaurant 中以 meal.seasonal_fruit 访问则编译检查不通过。
如果是应用于枚举,则枚举中的成员都是公开的。
mod back_of_house {
pub enum Appetizer {
Soup,
Salad,
}
}
pub fn eat_at_restaurant() {
let order1 = back_of_house::Appetizer::Soup;
let order2 = back_of_house::Appetizer::Salad;
}
使用 use 将路径导入作用域
示例代码
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
以上代码中,使用了 use 后,如同 hosting 模块被定义在根节点下一样,使用 use 将路径引入作用域时也需要遵守私有性规则。若将 eat_at_restaurant 移动到新模块中,则编译检查不通过
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
use crate::front_of_house::hosting;
mod customer {
// 编译不通过
// 改为 super::hosting::add_to_waitlist();
// 或将 use 移动到此处
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
}
引入相同名称类型时的处理方式
示例代码
use std::fmt;
use std::io;
fn function1() -> fmt::Result {
// ...
}
fn function2() -> io::Result {
// ...
}
如果使用
use std::fmt::Result;
use std::io::Result;
就会出现问题,Rust 无法在我们使用 Result 时确定使用的是哪一个 Result,解决方法可使用 as 来提供新名称
use std::fmt::Result;
use std::io::Result as IoResult;
fn function1() -> Result {
// ...
}
fn function2() -> IoResult<()> {
// ...
}
这样就可以避免发生名称冲突。
使用 pub use 重导出名称
示例代码
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
pub use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
如果没有上述修改,外部调用 add_to_waitlist 时需使用
restaurant::front_of_house::hosting::add_to_waitlist()
修改后则可以通过这样来调用
// 对外部调用者视角来看
restaurant::hosting::add_to_waitlist()
// 对库代码编写者视角来看
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
// 二者都为 hosting::add_to_waitlist();
重导出名称后,就相当于 hosting 好像在这段代码中作用域定义了一样。当代码的内部结构与外部所期望的访问结构不同时,重导出技术会变得非常有用,通过使用 pub use,我们可以在编写代码时使用一种结构,而对外暴露时使用另一种不同的结构,这一方法可以让我们的代码库对编写者与调用者同时保持良好的组织结构。
使用嵌套路径将多个条目从同一包或同一模块中引入作用域
use std::io;
use std::io::Write;
合并为
use std::io::{self, Write};
如果想将所有公共条目引入,可使用通配符
use std::collections::*;
总结
Rust 允许将包拆分为不同的单元包,并将单元包拆分为不同的模块,这样就能够在其它模块中引用某个特定模块内定义的条目。为了引用外部条目,可以通过 use 语句将路径引入作用域,接着在作用域使用较短的路径多次使用对应的条目。模块中的代码是私有的,可以通过添加 pub 关键字将定义声明为公共的。
