包(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的根模块
  • 包(Package):
    • 包由一个或多个crates组成,可以提供一系列功能。包中含有Cargo.toml文件,描述了如何使用这些crates
    • 包中可以包含至多一个Library Crate,包含任意多个Binary Crate,但必须至少包含一个Crate(无论是Library还是Binary)

让我们来看看创建包的时候会发生什么。执行cargo new

1
2
3
4
5
6
7
$ cargo new my-project
Created binary (application) `my-project` package
$ ls my-project
Cargo.toml
src
$ ls my-project/src
main.rs

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.rssrc/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
  • 声明模块:在crate root文件中,你可以使用mod [模块名]声明一个新的模块,例如用mod garden声明一个名为“garden”的模块,编译器会在以下几个位置寻找该模块包含的具体内容:
    • 内联(inline),找mod garden后边花括号中的内容(不是以分号结尾的那个)
      • 分号结尾的代码只是模块的声明,它告诉Rust要在另一个与模块同名的文件中加载模块内容
        • mod garden;是告诉Rust要在与garden同名的文件中加载模块garden的真正内容
    • 文件src/garden.rs
    • 文件src/garden/mod.rs
  • 声明子模块(submodules):除了crate root文件之外,crate中任何其它文件里,可以声明子模块(如mod vegetables;)。编译器会在下面位置寻找该模块包含的具体内容:
    • 内联(inline),找mod vegetables后边花括号中的内容,而不是以分号结尾的那个
    • 文件src/garden/vegetables.rs
    • 文件src/garden/vegetables/mod.rs
  • 模块中代码的路径:一旦一个模块被作为crate的一部分进行编译,就可以在那个crate中的其它地方引用这个模块(使用路径来引用)
    • 例如,引用garden vegetables模块中的Asparagus类型: crate::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
2
3
4
5
6
7
8
backyard
├── Cargo.lock
├── Cargo.toml
└── src
├── garden
│   └── vegetables.rs
├── garden.rs
└── main.rs

crate root文件是src/main.rs,内容为:

1
2
3
4
5
6
7
8
use crate::garden::vegetables::Asparagus;

pub mod garden;

fn main() {
let plant = Asparagus {};
println!("I'm growing {:?}!", plant);
}

pub mod garden;意思是编译器将src/garden.rs中的代码添加进来。src/garden.rs中的内容是:

1
pub mod vegetables;

意思是src/garden/vegetables.rs中的代码也要添加进来。

src/garden/vegetables.rs的内容:

1
2
#[derive(Debug)]
pub struct Asparagus {}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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() {}
}
}

模块内部可以有其他模块(如front_of_house中有hostingserving)。模块中可以定义其它事项,如:结构体、枚举、常量、traits等等。

通过使用模块,我们可以将相关定义分组在一起,并说出它们相关联的原因。

之前我们把src/main.rssrc/lib.rs称为crate root。这样称呼是因为,这两个文件中的内容组成了一个名为crate的模块,是模块结构的根,称为模块树(module tree):

1
2
3
4
5
6
7
8
9
crate
└── front_of_house
├── hosting
│ ├── add_to_waitlist
│ └── seat_at_table
└── serving
├── take_order
├── serve_order
└── take_payment

模块树显示了:

  • 一些模块是如何相互嵌套的(例如,在front_of_house内嵌套hosting)。
  • 一些模块是彼此的兄弟姐妹(siblings),意味着它们是在同一模块中定义的(例如hostingserving都在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
2
3
4
5
6
7
8
9
10
11
12
13
mod front_of_house {
mod hosting {
fn add_to_waitlist() {}
}
}

pub fn eat_at_restaurant() {
// Absolute path:由于本函数与`add_to_waitlist()`在同一个crate下,可以直接用`crate`关键字来引用
crate::front_of_house::hosting::add_to_waitlist();

// Relative path:从与定义`eat_at_restaurant`的模块同一level的模块开始引用
front_of_house::hosting::add_to_waitlist();
}

这段代码会有编译错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
$ cargo build
Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0603]: module `hosting` is private
--> src/lib.rs:9:28
|
9 | crate::front_of_house::hosting::add_to_waitlist();
| ^^^^^^^ private module
|
note: the module `hosting` is defined here
--> src/lib.rs:2:5
|
2 | mod hosting {
| ^^^^^^^^^^^

error[E0603]: module `hosting` is private
--> src/lib.rs:12:21
|
12 | front_of_house::hosting::add_to_waitlist();
| ^^^^^^^ private module
|
note: the module `hosting` is defined here
--> src/lib.rs:2:5
|
2 | mod hosting {
| ^^^^^^^^^^^

For more information about this error, try `rustc --explain E0603`.
error: could not compile `restaurant` due to 2 previous errors

意思是hosting模块是私有的(private)。

模块不仅对组织代码有用,对privacy boundary的定义也很有用。如果你想让一个项是私有的(如函数、结构体等),就把它放进模块中。

在Rust中,所有项默认都是私有(private)的。父模块中的项不能使用子模块中的私有项,但子模块中的项可以用它们祖先的项。

3.2. 用pub关键字暴露路径

回到上部分hosting模块是私有的这个编译错误,我们用pub关键字将hosting变成共有的:

1
2
3
4
5
6
7
8
9
10
11
12
13
mod front_of_house {
pub mod hosting {
fn add_to_waitlist() {}
}
}

pub fn eat_at_restaurant() {
// Absolute path
crate::front_of_house::hosting::add_to_waitlist();

// Relative path
front_of_house::hosting::add_to_waitlist();
}

依然有编译错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
$ cargo build
Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0603]: function `add_to_waitlist` is private
--> src/lib.rs:9:37
|
9 | crate::front_of_house::hosting::add_to_waitlist();
| ^^^^^^^^^^^^^^^ private function
|
note: the function `add_to_waitlist` is defined here
--> src/lib.rs:3:9
|
3 | fn add_to_waitlist() {}
| ^^^^^^^^^^^^^^^^^^^^

error[E0603]: function `add_to_waitlist` is private
--> src/lib.rs:12:30
|
12 | front_of_house::hosting::add_to_waitlist();
| ^^^^^^^^^^^^^^^ private function
|
note: the function `add_to_waitlist` is defined here
--> src/lib.rs:3:9
|
3 | fn add_to_waitlist() {}
| ^^^^^^^^^^^^^^^^^^^^

For more information about this error, try `rustc --explain E0603`.
error: could not compile `restaurant` due to 2 previous errors

为什么?在mod hosting前加pubhosting模块变成public的,但模块内的内容仍然是private的。因此,让模块public并不能让模块内的内容public

add_to_waitlist变成public:

1
2
3
4
5
6
7
8
9
10
11
12
13
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}

pub fn eat_at_restaurant() {
// Absolute path
crate::front_of_house::hosting::add_to_waitlist();

// Relative path
front_of_house::hosting::add_to_waitlist();
}

现在代码可以编译了。

3.3. 使用以super起始的相对路径

super调用父模块中的函数:

1
2
3
4
5
6
7
8
9
10
fn deliver_order() {}

mod back_of_house {
fn fix_incorrect_order() {
cook_order();
super::deliver_order();
}

fn cook_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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
mod back_of_house {
pub struct Breakfast { // public
pub toast: String, // public
seasonal_fruit: String, // private
}

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");
}

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_fruitfield in eat_at_restaurant.

3.4.2. 公开枚举

与结构体相反的是,如果我们让一个枚举类型public,它的所有成员都是public的:

1
2
3
4
5
6
7
8
9
10
11
mod back_of_house {
pub enum Appetizer { // 只需要在这里添加`pub`
Soup,
Salad,
}
}

pub fn eat_at_restaurant() {
let order1 = back_of_house::Appetizer::Soup;
let order2 = back_of_house::Appetizer::Salad;
}

4. 使用 use 关键字将路径引入作用域

4.1. use用法示例

使用use创建快捷方式,以后只需要使用短名字就可以了,类似于在文件系统中创建符号连接(symbolic link)。

1
2
3
4
5
6
7
8
9
10
11
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起作用的范围。它只能为同一作用域内的代码创建快捷方式,如果将eat_at_restaurant放进一个新的子模块中,再从中使用hosting::add_to_waitlist();,就会出现编译错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}

use crate::front_of_house::hosting;

mod customer {
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
}

快捷方式不适用于customer模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ cargo build
Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0433]: failed to resolve: use of undeclared crate or module `hosting`
--> src/lib.rs:11:9
|
11 | hosting::add_to_waitlist();
| ^^^^^^^ use of undeclared crate or module `hosting`

warning: unused import: `crate::front_of_house::hosting`
--> src/lib.rs:7:5
|
7 | use crate::front_of_house::hosting;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: `#[warn(unused_imports)]` on by default

For more information about this error, try `rustc --explain E0433`.
warning: `restaurant` (lib) generated 1 warning
error: could not compile `restaurant` due to previous error; 1 warning emitted

4.2. 创建惯用的use路径

惯例是use到所调用函数的上一层(模块),而不是一直到所调用的函数,如:

1
2
3
use crate::front_of_house::hosting; // 这是惯例。这样,在调用函数时会指明函数所在的父模块,
// 清晰地显示出该函数不是本地函数
use crate::front_of_house::hosting::add_to_waitlist; // 可以工作,但一般不这样用

同名函数调用:

1
2
3
4
5
6
7
8
9
10
11
12
use std::fmt;
use std::io;

fn function1() -> fmt::Result {
// --snip--
Ok(())
}

fn function2() -> io::Result<()> {
// --snip--
Ok(())
}

4.3. 使用as创建别名

需要调用两个同名函数时,还可以用as创建别名:

1
2
3
4
5
6
7
8
9
10
11
12
use std::fmt::Result;
use std::io::Result as IoResult;

fn function1() -> Result {
// --snip--
Ok(())
}

fn function2() -> IoResult<()> {
// --snip--
Ok(())
}

4.4. 用pub use重新导出名称

使用场景:

  • 将一个项纳入作用域
  • 让调用我们代码的程序也能将该项纳入他们的作用域

例:

1
2
3
4
5
6
7
8
9
10
11
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();
}

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
2
3
4
5
use rand::Rng;  // here, we bring `Rng` trait into scope

fn main() {
let secret_number = rand::thread_rng().gen_range(1..=100);
}

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
2
use std::cmp::Ordering;
use std::io;

我们可以使用嵌套路径将相同的项在一行中引入作用域。这么做需要指定路径的相同部分,接着是两个冒号,接着是大括号中的各自不同的路径部分:

1
use std::{cmp::Ordering, io};

又如:

1
2
use std::io;
use std::io::Write;

可以写成:

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
2
3
4
5
6
7
8
9
10
mod front_of_house;  // 声明`front_of_house`模块,
// 编译器将到`src/front_of_house.rs`中寻找模块的具体内容

pub use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
hosting::add_to_waitlist();
hosting::add_to_waitlist();
}

对以src/main.rs为crate root文件的Binary Crate也是一样。

mod front_of_house后使用分号,而不是代码块,这将告诉Rust在另一个与模块同名的文件中加载模块的内容。

src/front_of_house.rs的内容:

1
2
3
pub mod hosting {
pub fn add_to_waitlist() {}
}

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