当你编写大型程序时,组织你的代码显得尤为重要,因为你想在脑海中通晓整个程序,那几乎是不可能完成的。通过对相关功能进行分组和划分不同功能的代码,你可以清楚在哪里可以找到实现了特定功能的代码,以及在哪里可以改变一个功能的工作方式。
到目前为止,我们编写的程序都在一个文件的一个模块中。伴随着项目的增长,你可以通过将代码分解为多个模块和多个文件来组织代码。一个包可以包含多个二进制 crate 项和一个可选的 crate 库。伴随着包的增长,你可以将包中的部分代码提取出来,做成独立的 crate,这些 crate 则作为外部依赖项。
Rust 有许多功能可以让你管理代码的组织,包括哪些内容可以被公开,哪些内容作为私有部分,以及程序每个作用域中的名字。这些功能。这有时被称为 “模块系统(the module system)”,包括:
crate 是一个二进制项或者库。crate root 是一个源文件,Rust 编译器以它为起始点,并构成你的 crate 的根模块。
*包*(*package*) 是提供一系列功能的一个或者多个 crate。一个包会包含有一个 Cargo.toml 文件,阐述如何去构建这些 crate。
包中所包含的内容由几条规则来确立。一个包中至多 只能 包含一个库 crate(library crate);包中可以包含任意多个二进制 crate(binary crate);包中至少包含一个 crate,无论是库的还是二进制的。
当我们运行 cargo new new-project
时,Cargo 会给我们的包创建一个 Cargo.toml 文件。查看 Cargo.toml 的内容,会发现并没有提到 *src/main.rs*,因为 Cargo 遵循的一个约定:src/main.rs 就是一个与包同名的二进制 crate 的 crate 根。同样的,Cargo 知道如果包目录中包含 *src/lib.rs*,则包带有与其同名的库 crate,且 src/lib.rs 是 crate 根。crate 根文件将由 Cargo 传递给 rustc
来实际构建库或者二进制项目。
在此,我们有了一个只包含 src/main.rs 的包,意味着它只含有一个名为 my-project
的二进制 crate。如果一个包同时含有 src/main.rs 和 *src/lib.rs*,则它有两个 crate:一个库和一个二进制项,且名字都与包相同。通过将文件放在 src/bin 目录下,一个包可以拥有多个二进制 crate:每个 src/bin 下的文件都会被编译成一个独立的二进制 crate。像这样
├── Cargo.lock
├── Cargo.toml
├── README.md
├── src
├── bin
│ ├── main2.rs // crate 2 这里有一个 fn main()
│ └── main.rs // crate 1 这里有一个 fn main()
└── lib.rs // crate 3
模块 让我们可以将一个 crate 中的代码进行分组,以提高可读性与重用性。模块还可以控制项的 *私有性*,即项是可以被外部代码使用的(*public*),还是作为一个内部实现的内容,不能被外部代码使用(*private*)。
我们用关键字 mod
定义一个模块,指定模块的名字(在示例中为 front_of_house
),并用大括号包围模块的主体。我们可以在模块中包含其他模块,就像本示例中的 hosting
和 serving
模块。模块中也可以包含其他项,比如结构体、枚举、常量、trait,函数。
mod front_of_house {
mod hosting {
fn add_to_waitlist() {}
fn seat_at_table() {}
}
mod serving {
fn take_order() {}
fn server_order() {}
fn take_payment() {}
}
}
通过使用模块,我们可以把相关的定义组织起来,并通过模块命名来解释为什么它们之间有相关性。使用这部分代码的开发者可以更方便的循着这种分组找到自己需要的定义,而不需要通览所有。编写这部分代码的开发者通过分组知道该把新功能放在哪里以便继续让程序保持组织性。
之前我们提到,src/main.rs 和 src/lib.rs 被称为 crate 根。如此称呼的原因是,这两个文件中任意一个的内容会构成名为 crate
的模块,且该模块位于 crate 的被称为 模块树 的模块结构的根部(”at the root of the crate’s module structure”)。而上面定义的 lib 的模块树则会像这样:
crate
└── front_of_house
├── hosting
│ ├── add_to_waitlist
│ └── seat_at_table
└── serving
├── take_order
├── serve_order
└── take_payment
这个树展示了模块间是如何相互嵌套的(比如,hosting
嵌套在 front_of_house
内部)。这个树还展示了一些模块互为 兄弟 ,即它们被定义在同一模块内(hosting
和 serving
都定义在 front_of_house
内)。继续使用家族比喻,如果模块 serving
包含在模块 front_of_house
的内部,我们称模块 serving
是模块 front_of_house
的 孩子 且模块 front_of_house
是模块 serving
的 父辈 。注意整个模块树的根位于名为 crate
的隐式模块下。
来看一下 Rust 如何在模块树中找到一个项的位置,我们使用路径的方式,就像在文件系统使用路径一样。如果我们想要调用一个函数,我们需要知道它的路径。
路径有两种形式:
crate
开头。self
、super
或当前模块的标识符开头。绝对路径和相对路径都后跟一个或多个由双冒号(::
)分割的标识符。
模块不仅对于你组织代码很有用。他们还定义了 Rust 的 *私有性边界*(*privacy boundary*):这条界线不允许外部代码了解、调用和依赖被封装的实现细节。所以,如果你希望创建一个私有函数或结构体,你可以将其放入模块。
Rust 中默认所有项(函数、方法、结构体、枚举、模块和常量)都是私有的。父模块中的项不能使用子模块中的私有项,但是子模块中的项可以使用他们父模块中的项。这是因为子模块封装并隐藏了他们的实现详情,但是子模块可以看到他们定义的上下文。继续拿餐馆作比喻,把私有性规则想象成餐馆的后台办公室:餐馆内的事务对餐厅顾客来说是不可知的,但办公室经理可以洞悉其经营的餐厅并在其中做任何事情。
Rust 选择以这种方式来实现模块系统功能,因此默认隐藏内部实现细节。这样一来,你就知道可以更改内部代码的哪些部分而不会破坏外部代码。你还可以通过使用 pub
关键字来创建公共项,使子模块的内部部分暴露给上级模块。
mod front_of_house {
pub mod hosting { // 使模块公有并不使其内容也是公有的。模块上的 pub 关键字只允许其父模块引用它。
pub fn add_to_waitlist() {}
}
}
pub fn eat_at_restaurant() {
// Absolute path
crate::front_of_house::hosting::add_to_waitlist();
// Relative path 同 crate 所以可以省略 crate
front_of_house::hosting::add_to_waitlist();
}
在绝对路径,我们从 crate
,也就是 crate 根部开始。然后 crate 根部中定义了 front_of_house
模块。front_of_house
模块不是公有的,不过因为 eat_at_restaurant
函数与 front_of_house
定义于同一模块( crate
)中(即,eat_at_restaurant
和 front_of_house
是兄弟),我们可以从 eat_at_restaurant
中引用 front_of_house
。
super
起始的相对路径我们还可以使用 super
开头来构建从父模块开始的相对路径。这么做类似于文件系统中以 ..
开头的语法。我们为什么要这样做呢?
下面是一个例子,它模拟了厨师更正了一个错误订单,并亲自将其提供给客户的情况。fix_incorrect_order
函数通过指定的 super
起始的 serve_order
路径,来调用 serve_order
函数:
fn serve_order() {}
mod back_of_house {
fn fix_incorrect_order() {
cook_order();
super::serve_order();
}
fn cook_order() {}
}
fix_incorrect_order
函数在 back_of_house
模块中,所以我们可以使用 super
进入 back_of_house
父模块,也就是本例中的 crate
根。在这里,我们可以找到 serve_order
。成功!我们认为 back_of_house
模块和 serve_order
函数之间可能具有某种关联关系,并且,如果我们要重新组织这个 crate 的模块树,需要一起移动它们。因此,我们使用 super
,这样一来,如果这些代码被移动到了其他模块,我们只需要更新很少的代码。
我们还可以使用 pub
来设计公有的结构体和枚举,不过有一些额外的细节需要注意。
pub
,这个结构体会变成公有的,但是这个结构体的字段仍然是私有的。我们可以根据情况决定每个字段是否公有。enum
关键字前面加上 pub
mod back_of_house {
pub enum Appetizer {
Soup,
Salad,
}
pub struct Breakfast {
pub toast: String,
seasonal_fruit: String,
}
impl Breakfast {
pub fn summer(toast: &str) -> Breakfast {
Breakfast {
toast: String::from(toast),
seasonal_fruit: String::from("peaches"),
}
}
}
}
pub fn eat_at_restaurant() {
// Order a breakfast in the summer with Rye toast
let mut meal = back_of_house::Breakfast::summer("Rye");
// Change our mind about what bread we'd like
meal.toast = String::from("Wheat");
println!("I'd like {} toast please", meal.toast);
// The next line won't compile if we uncomment it; we're not allowed
// to see or modify the seasonal fruit that comes with the meal
// meal.seasonal_fruit = String::from("blueberries");
let order1 = back_of_house::Appetizer::Soup;
let order2 = back_of_house::Appetizer::Salad;
}
因为我们创建了名为 Appetizer
的公有枚举,所以我们可以在 eat_at_restaurant
中使用 Soup
和 Salad
成员。如果枚举成员不是公有的,那么枚举会显得用处不大;给枚举的所有成员挨个添加 pub
是很令人恼火的,因此枚举成员默认就是公有的。
结构体通常使用时,不必将它们的字段公有化,因此结构体遵循常规,内容全部是私有的,除非使用 pub
关键字。
use
关键字将名称引入作用域在作用域中增加 use
和路径类似于在文件系统中创建软连接(符号连接,symbolic link)。通过在 crate 根增加 use crate::front_of_house::hosting
,现在 hosting
在作用域中就是有效的名称了,如同 hosting
模块被定义于 crate 根一样。通过 use
引入作用域的路径也会检查私有性,同其它路径一样。
你还可以使用 use
和相对路径来将一个项引入作用域。
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
use front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
hosting::add_to_waitlist();
hosting::add_to_waitlist();
}
as
关键字提供新的名称我们想使用 use
语句将两个具有相同名称的项带入作用域,因为 Rust 不允许这样做。如果我们是指定 use std::fmt::Result
和 use std::io::Result
,我们将在同一作用域拥有了两个 Result
类型,当我们使用 Result
时,Rust 则不知道我们要用的是哪个。我们可以通过 as
重命名其中一个 Result
类型。
use std::fmt::Result;
use std::io::Result as IoResult;
fn function1() -> Result {
// --snip--
}
fn function2() -> IoResult<()> {
// --snip--
}
pub use
重导出名称当使用 use
关键字将名称导入作用域时,在新作用域中可用的名称是私有的。如果为了让调用你编写的代码的代码能够像在自己的作用域内引用这些类型,可以结合 pub
和 use
。这个技术被称为 “*重导出*(*re-exporting*)”,因为这样做将项引入作用域并同时使其可供其他代码引入自己的作用域。
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
pub use crate::front_of_house::hosting;
// 外部可以使用 lib::hosting::add_to_waitlist() 来调用 add_to_waitlist
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
hosting::add_to_waitlist();
hosting::add_to_waitlist();
}
如果希望将一个路径下 所有 公有项引入作用域,可以指定路径后跟 *
,glob 运算符:
use std::collections::*;
这个 use
语句将 std::collections
中定义的所有公有项引入当前作用域。使用 glob 运算符时请多加小心!Glob 会使得我们难以推导作用域中有什么名称和它们是在何处定义的。
glob 运算符经常用于测试模块 tests
中,这时会将所有内容引入作用域。
到目前为止,本章所有的例子都在一个文件中定义多个模块。当模块变得更大时,你可能想要将它们的定义移动到单独的文件中,从而使代码更容易阅读。
mod front_of_house;
pub use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
hosting::add_to_waitlist();
hosting::add_to_waitlist();
}
在 mod front_of_house
后使用分号,而不是代码块,这将告诉 Rust 在另一个与模块同名的文件中加载模块的内容。继续重构我们例子,将 hosting
模块也提取到其自己的文件中,仅对 src/front_of_house.rs 包含 hosting
模块的声明进行修改:
文件名: src/front_of_house.rs
pub mod hosting;
接着我们创建一个 src/front_of_house 目录和一个包含 hosting
模块定义的 src/front_of_house/hosting.rs 文件:
文件名: src/front_of_house/hosting.rs
pub fn add_to_waitlist() {}
模块树依然保持相同,eat_at_restaurant
中的函数调用也无需修改继续保持有效,即便其定义存在于不同的文件中。这个技巧让你可以在模块代码增长时,将它们移动到新文件中。
Rust 提供了将包分成多个 crate,将 crate 分成模块,以及通过指定绝对或相对路径从一个模块引用另一个模块中定义的项的方式。你可以通过使用 use
语句将路径引入作用域,这样在多次使用时可以使用更短的路径。模块定义的代码默认是私有的,不过可以选择增加 pub
关键字使其定义变为公有。
好好学习,天天向上