包(Packages), Crates和模块(Modules)
本文大部分内容翻译自:The Rust Programming Language
Rust的“模块系统(the module system)”:
- 包(Packages):Cargo的一个功能,允许你构建、测试和分享crate。
- Crates:一个模块的树形结构,它形成了库或二进制项目。
- 模块(Modules)和use:允许你控制作用域和路径的私有性。
- 路径(path):一个命名的方式(如命名结构体、函数或模块等项)
总体来说,Rust中:
- 包被分成一个或多个crates,crates被分成模块
- 通过指定绝对或相对路径从一个模块引用另一个模块中定义的项
- 通过
use
关键字将路径引入作用域,以便多次使用时可以使用更短的路径 - 模块定义的代码默认是私有的,不过可以选择增加
pub
关键字使其变为公有
1. Packages(包)和Crates
- Crate:分为Binary Crates和Library Crates
- Binary Crates:是你可以编译成可执行文件来运行的程序,必须有
main
函数 - Library Crates:没有
main
函数,不会编译成可执行文件。它们的目的是提供一些可被其它项目使用的功能 crate root
是一个源文件,Rust编译器以它为起始点,然后构建你的crate
的根模块
- Binary Crates:是你可以编译成可执行文件来运行的程序,必须有
- 包(Package):
- 包由一个或多个crates组成,可以提供一系列功能。包中含有
Cargo.toml
文件,描述了如何使用这些crates - 包中可以包含至多一个Library Crate,包含任意多个Binary Crate,但必须至少包含一个Crate(无论是Library还是Binary)
- 包由一个或多个crates组成,可以提供一系列功能。包中含有
让我们来看看创建包的时候会发生什么。执行cargo new
:
1 | $ cargo new my-project |
Cargo会给我们的包创建一个Cargo.toml
文件。查看Cargo.toml
的内容,会发现并没有提到src/main.rs
,因为 Cargo遵循一个惯例:src/main.rs
就是与包同名的Binary Crate的crate root。
同样,Cargo知道如果包目录中包含src/lib.rs
,则包带有与其同名的Library Crate,且src/lib.rs
是crate root。crate根文件将由Cargo传递给rustc
来构建library或者binary项目。
这里,我们有了一个只包含src/main.rs
的包,意味着它只含有一个名为my-project
的Bianry Crate。
如果一个包同时含有src/main.rs
和src/lib.rs
,则它有两个crates:一个二进制的(binary)和一个库的(library),且名字都与包相同。
通过将文件放在src/bin
目录下,一个包可以拥有多个binary crates:每个src/bin
下的文件都会被编译成一个独立的二进制 crate。
2. 定义模块来控制作用域与私有性
2.1. 模块、路径、use
关键字、pub
关键字简介
模块(modules)、路径(paths)、use
关键字、pub
关键字在编译器中的工作机制:
- 起始点为crate root:当编译一个crate时,编译器先查看crate root
- 对于Library Crate,通常是
src/lib.rs
- 对于Binary Crate,通常是
src/main.rs
- 对于Library Crate,通常是
- 声明模块:在crate root文件中,你可以使用
mod [模块名]
声明一个新的模块,例如用mod garden
声明一个名为“garden”的模块,编译器会在以下几个位置寻找该模块包含的具体内容:- 内联(inline),找
mod garden
后边花括号中的内容(不是以分号结尾的那个)- 分号结尾的代码只是模块的声明,它告诉Rust要在另一个与模块同名的文件中加载模块内容
- 如
mod garden;
是告诉Rust要在与garden
同名的文件中加载模块garden
的真正内容
- 如
- 分号结尾的代码只是模块的声明,它告诉Rust要在另一个与模块同名的文件中加载模块内容
- 文件
src/garden.rs
- 文件
src/garden/mod.rs
- 内联(inline),找
- 声明子模块(submodules):除了crate root文件之外,crate中任何其它文件里,可以声明子模块(如
mod vegetables;
)。编译器会在下面位置寻找该模块包含的具体内容:- 内联(inline),找
mod vegetables
后边花括号中的内容,而不是以分号结尾的那个 - 文件
src/garden/vegetables.rs
- 文件
src/garden/vegetables/mod.rs
- 内联(inline),找
- 模块中代码的路径:一旦一个模块被作为crate的一部分进行编译,就可以在那个crate中的其它地方引用这个模块(使用路径来引用)
- 例如,引用garden vegetables模块中的Asparagus类型:
crate::garden::vegetables::Asparagus
- 例如,引用garden vegetables模块中的Asparagus类型:
- 私有和公有(Private vs Public):默认情况下,模块中的代码是其父模块的私有代码。要公开模块,要用
pub mod
来声明它,而不是用mod
。要使public模块中的项目也public,要在它们的声明(declaration)代码之前使用pub
。 use
关键字:在作用域内,use
关键字可以创建使用其他项目的快捷方式。例如,在任何可以引用crate::garden::vegetables::Asparagus
的作用域中,可以先用use crate::garden::vegetables::Asparagus
创建一个快捷方式,然后在需要使用Asparagus
类型时,直接用Asparagus
即可。
举例:Binary Crate “backyard”:
1 | backyard |
crate root文件是src/main.rs
,内容为:
1 | use crate::garden::vegetables::Asparagus; |
pub mod garden;
意思是编译器将src/garden.rs
中的代码添加进来。src/garden.rs
中的内容是:
1 | pub mod vegetables; |
意思是src/garden/vegetables.rs
中的代码也要添加进来。
src/garden/vegetables.rs
的内容:
1 |
|
2.2. 将相关联的代码组合到模块中
模块允许我们将crate中的代码组合成group,以便于阅读和重用。模块还控制项目的privacy,即项目是否能被外部代码(public)使用,还是它是内部的实现细节,不能给外部使用(private)。
例如,我们写一个提供餐厅功能的Library Crate。
在餐饮业,餐厅的某些部分被称为“front of house”,另一些地方被称为“back of house”。“front of house”是客户所在的地方,是接待客户、服务员接受订单和付款、调酒师制作饮料的地方。“back of house”是主厨和厨师工作、洗碗机工作,经理做行政工作的地方。
为了用与真正的餐厅相同的方式构建crate,我们可以将项目组织成嵌套模块。
运行cargo new --lib restaurant
创建一个名为餐厅的新库,src/lib.rs的内容如下:
1 | mod front_of_house { |
模块内部可以有其他模块(如front_of_house
中有hosting
和serving
)。模块中可以定义其它事项,如:结构体、枚举、常量、traits等等。
通过使用模块,我们可以将相关定义分组在一起,并说出它们相关联的原因。
之前我们把src/main.rs
和src/lib.rs
称为crate root。这样称呼是因为,这两个文件中的内容组成了一个名为crate
的模块,是模块结构的根,称为模块树(module tree):
1 | crate |
模块树显示了:
- 一些模块是如何相互嵌套的(例如,在
front_of_house
内嵌套hosting
)。 - 一些模块是彼此的兄弟姐妹(siblings),意味着它们是在同一模块中定义的(例如
hosting
和serving
都在front_of_house
中定义)。 - 为了延续家族隐喻,如果模块A包含在模块B中,我们说模块A是模块B的子模块,模块B是模块A的父模块。
注意,整棵模块树都基于暗含的、名为crate
的模块下。
3. 引用模块项目的路径
3.1. 模块引用
- 绝对路径:绝对路径以crate root为起点。
- 如果是外部crate要引用本crate,要使用被引用的crate的名字;
- 如果是引用同一个crate下的内容,可以直接使用
crate
关键字来引用
- 相对路径
在eat_at_restaurant
中使用add_to_waitlist
函数:
1 | mod front_of_house { |
这段代码会有编译错误:
1 | $ cargo build |
意思是hosting
模块是私有的(private)。
模块不仅对组织代码有用,对privacy boundary的定义也很有用。如果你想让一个项是私有的(如函数、结构体等),就把它放进模块中。
在Rust中,所有项默认都是私有(private)的。父模块中的项不能使用子模块中的私有项,但子模块中的项可以用它们祖先的项。
3.2. 用pub
关键字暴露路径
回到上部分hosting
模块是私有的这个编译错误,我们用pub
关键字将hosting
变成共有的:
1 | mod front_of_house { |
依然有编译错误:
1 | $ cargo build |
为什么?在mod hosting
前加pub
让hosting
模块变成public的,但模块内的内容仍然是private的。因此,让模块public并不能让模块内的内容public。
将add_to_waitlist
变成public:
1 | mod front_of_house { |
现在代码可以编译了。
3.3. 使用以super
起始的相对路径
用super
调用父模块中的函数:
1 | fn deliver_order() {} |
3.4. 公开结构体和枚举
3.4.1. 公开结构体
If we use pub before a struct definition, we make the struct public, but the struct’s fields will still be private. We can make each field public or not on a case-by-case basis.
1 | mod back_of_house { |
Also, note that because back_of_house::Breakfast
has a private field, the struct needs to provide a public associated function that constructs an instance of Breakfast
(we’ve named it summer
here). If Breakfast
didn’t have such a function, we couldn’t create an instance of Breakfast
in eat_at_restaurant
because we couldn’t set the value of the private seasonal_fruit
field in eat_at_restaurant
.
3.4.2. 公开枚举
与结构体相反的是,如果我们让一个枚举类型public,它的所有成员都是public的:
1 | mod back_of_house { |
4. 使用 use 关键字将路径引入作用域
4.1. use
用法示例
使用use
创建快捷方式,以后只需要使用短名字就可以了,类似于在文件系统中创建符号连接(symbolic link)。
1 | mod front_of_house { |
注意use
起作用的范围。它只能为同一作用域内的代码创建快捷方式,如果将eat_at_restaurant
放进一个新的子模块中,再从中使用hosting::add_to_waitlist();
,就会出现编译错误:
1 | mod front_of_house { |
快捷方式不适用于customer
模块:
1 | $ cargo build |
4.2. 创建惯用的use
路径
惯例是use
到所调用函数的上一层(模块),而不是一直到所调用的函数,如:
1 | use crate::front_of_house::hosting; // 这是惯例。这样,在调用函数时会指明函数所在的父模块, |
同名函数调用:
1 | use std::fmt; |
4.3. 使用as
创建别名
需要调用两个同名函数时,还可以用as
创建别名:
1 | use std::fmt::Result; |
4.4. 用pub use
重新导出名称
使用场景:
- 将一个项纳入作用域
- 让调用我们代码的程序也能将该项纳入他们的作用域
例:
1 | mod front_of_house { |
Before this change, external code would have to call the add_to_waitlist
function by using the path restaurant::front_of_house::hosting::add_to_waitlist()
. Now that this pub use
has re-exported the hosting module from the root module, external code can now use the path restaurant::hosting::add_to_waitlist()
instead.
With pub use
, we can write our code with one structure but expose a different structure.
4.5. 使用外部包
To use rand
in the project, we need to add this line to Cargo.toml
:
1 | rand = "0.8.3" |
Adding rand
as a dependency in Cargo.toml
tells Cargo to download the rand
package and any dependencies from crates.io
and make rand
available to our project.
Then, to bring rand
definitions into the scope of our package, we added a use
line starting with the name of the crate, rand
, and listed the items we wanted to bring into scope:
1 | use rand::Rng; // here, we bring `Rng` trait into scope |
Rust社区的成员在cates.io
上提供了许多软件包,要使用它们,都会涉及以下相同的步骤:
- 将它们列在包的
Cargo.toml
文件中 - 使用
use
将crate中的内容纳入作用域
Note that the standard library (std
) is also a crate that’s external to our package. Because the standard library is shipped with the Rust language, we don’t need to change Cargo.toml
to include std
. But we do need to refer to it with use to bring items from there into our package’s scope. For example, with HashMap we would use this line:
1 | use std::collections::HashMap; |
This is an absolute path starting with std, the name of the standard library crate.
4.6. 嵌套路径来消除大量的use
行
当需要引入很多定义于相同包或相同模块的项时,为每一项单独列出一行会占用源码很大的空间。例如:
1 | use std::cmp::Ordering; |
我们可以使用嵌套路径将相同的项在一行中引入作用域。这么做需要指定路径的相同部分,接着是两个冒号,接着是大括号中的各自不同的路径部分:
1 | use std::{cmp::Ordering, io}; |
又如:
1 | use std::io; |
可以写成:
1 | use std::io::{self, Write}; |
4.7. 通过glob
运算符将所有的公有定义引入作用域
如果希望将一个路径下所有公有项引入作用域,可以在路径后加*
glob运算符:
1 | use std::collections::*; |
这个use语句将std::collections
中定义的所有公有项引入当前作用域。
使用glob运算符时要多加小心!Glob会使我们难以推导作用域中有什么名称和它们是在何处定义的。
glob运算符经常用于测试模块tests中,这时会将所有内容引入作用域。
5. 将模块拆分成多个文件
到目前为止,所有的例子都在一个文件中定义多个模块。当模块越来越大时,你可能想要将它们的定义移动到单独的文件中,从而使代码更容易阅读。
例如,将front_of_house
模块移动到属于它自己的文件src/front_of_house.rs
中,则crate root文件src/lib.rs
内容如下:
1 | mod front_of_house; // 声明`front_of_house`模块, |
对以src/main.rs
为crate root文件的Binary Crate也是一样。
在mod front_of_house
后使用分号,而不是代码块,这将告诉Rust在另一个与模块同名的文件中加载模块的内容。
src/front_of_house.rs
的内容:
1 | pub mod hosting { |
将hosting
模块也提取到它自己的文件中,对src/front_of_house.rs
中包含hosting
模块的声明进行修改:
1 | pub mod hosting; |
接着我们创建一个src/front_of_house
目录和一个包含hosting
模块定义的src/front_of_house/hosting.rs
文件:
1 | pub fn add_to_waitlist() {} |
模块树依然保持相同,eat_at_restaurant
中的函数调用也无需修改,继续保持有效,即便其定义存在于不同的文件中。这个技巧让你在模块代码增长时,将它们移动到新文件中。
注意,src/lib.rs
中的pub use crate::front_of_house::hosting
语句是没有改变的,在文件作为crate的一部分被编译时,use
不会有任何影响。mod
关键字声明了模块,Rust会在与模块同名的文件中查找模块的代码。
参考资料
[1] 使用包、Crate 和模块管理不断增长的项目:https://kaisery.github.io/trpl-zh-cn/ch07-00-managing-growing-projects-with-packages-crates-and-modules.html