目录
- [1. 包和crates](#1. 包和crates)
- [2. 定义模块来控制作用域与私有性](#2. 定义模块来控制作用域与私有性)
-
- [2.1 模块小抄](#2.1 模块小抄)
-
- [2.1.1 在模块中对相关代码进行分组](#2.1.1 在模块中对相关代码进行分组)
- [3. 引用模块树中项的路径](#3. 引用模块树中项的路径)
-
- [3.1 通过pub关键字暴露路径](#3.1 通过pub关键字暴露路径)
- [3.2 二进制和库crate包的最佳实践](#3.2 二进制和库crate包的最佳实践)
- [3.3 super开始的相对路径](#3.3 super开始的相对路径)
- [3.4 创建公有的结构体和枚举](#3.4 创建公有的结构体和枚举)
- [4. 使用use关键字将路径引入作用域](#4. 使用use关键字将路径引入作用域)
-
- [4.1 创建惯用的use路径](#4.1 创建惯用的use路径)
- [4.2 使用as关键字提供新的名称](#4.2 使用as关键字提供新的名称)
- [4.3 使用pub use重导出名称](#4.3 使用pub use重导出名称)
- [4.4 使用外部包](#4.4 使用外部包)
- [4.5 使用嵌套路径来清理大量的use列表](#4.5 使用嵌套路径来清理大量的use列表)
- [4.6 使用glob运算符导入项](#4.6 使用glob运算符导入项)
- [5. 将模块拆分成多个文件](#5. 将模块拆分成多个文件)
- 参考
1. 包和crates
crate是Rust编译器每次处理的最小代码单位。即使你用rustc而不是cargo来编译单个源代码文件,编译器也会把那个文件视为一个crate。crate可以包含模块,这些模块也可以定义在其他文件中,并与该crate一起编译。
crate有两种形式:二进制crate和库crate。二进制crate可以被编译为可执行程序,比如命令行程序或者服务端。它们必须有一个main函数来定义当程序被执行的时候所需要做的事情。之前所有创建的crate都是二进制crate。
库crate并没有main函数,它们也不会被编译为可执行程序。相反它们定义了可供多个项目复用的功能模块。比如rand crate提供了生成随机数的功能。
crate root是一个源文件,Rust编译器以它为起始点,并构成你的crate的根模块。后面会讲。
包package是提供一系列功能的一个或者多个crate的捆绑。一个包会包含一个Cargo.toml文件,阐述如何去构建这些crate。Cargo实际上就是一个包,它包含了用于构建你代码的命令行工具的二进制crate。其他项目也依赖Cargo库来实现与Cargo命令行程序一样的逻辑。
包中可以包含至多一个库。包中可以包含任意多个二进制crate,但是必须至少包含一个crate,无论是库的还是二进制的。
创建新的包:cargo new my-project。

Cargo在根目录给包创建了一个Cargo.toml文件,src/main.rs就是一个与包同名的二进制crate的crate根。同样的,如果src中是lib.rs,则包带有同名的库crate,src/lib.rs是crate根。crate根文件将由Cargo传递给rustc来实际构建库或者二级制项目。
如果一个包同时含有src/main.rs和src/lib.rs,则有两个crate------一个二进制的和一个库的,且名字都与包相同。通过将文件放在src/bin目录下,一个包可以拥有多个二进制crate,每个src/bin下的文件都会被编译成一个独立的二进制crate。
2. 定义模块来控制作用域与私有性
2.1 模块小抄
1、从crate根节点开始:当编译一个crate,编译器首先在crate根文件(src/lib.rs或src/main.rs)中寻找需要被编译的代码。
2、声明模块:在crate根文件中,你可以声明一个新模块,用mod xxx;声明模块。编译器会在下列路径中寻找模块代码:
内联,用大括号替换mod xxx后跟的分号。
在文件src/xxx.rs。
在文件src/xxx/mod.rs。
3、声明子模块:在除了crate根节点以外的任何文件中,可以定义子模块。可以在src/xxx中声明mod yyy;。编译器会在以父模块命名的目录中寻找子模块的代码:
内联,直接在mod yyy后跟大括号。
在文件src/xxx/yyy.rs。
在文件src/xxx/yyy/mod.rs。
4、模块中的代码路径:一旦一个模块是你crate的一部分,可以在隐私规则允许的前提下,从同一个crate内的任意地方,通过代码路径引用该模块的代码。比如,有一个xxx下yyy中的zzz类型可以通过crate::xxx::yyy::zzz访问。
5、私有VS公有:一个模块里的代码默认对其父模块私有。为了使一个模块公用,应当在声明时使用pub mod代替mod。为了使一个公用模块内部的成员公用,应当在声明前使用pub。
6、use关键字:在一个作用域内,use关键字创建了一个项的快捷方式,用来减少长路径的重复。在任何可以引用crate::xxx::yyy::zzz的作用域,可以通过use crate::xxx::yyy::zzz;来创建一个快捷方式,然后可以在作用域中只写zzz来使用该类型。
通过Cargo新建一个包backyard,文件目录如下:

这个例子的crate根文件是src/main.rs:
rust
use crate::garden::vegetables::Asparagus;
pub mod garden;
fn main() {
let plant = Asparagus {};
println!("I'm growing {plant:?}!");
}
pub mod garden;告诉编译器将src/garden.rs中发现的代码包含进来:
rust
pub mod vegetables;
pub mod vegetables;意味着在src/garden/vegetables.rs中的代码也应该被包含。
rust
#[derive(Debug)]
pub struct Asparagus {}
2.1.1 在模块中对相关代码进行分组
模块让我们可以将一个crate中的代码进行分组,以提高可用性与重用性。一个模块中的代码默认是私有的,还可以利用模块控制项的私有性。私有项是不可为外部使用的内在详细实现。也可以将模块和它其中的项标记为公开的,这样外部代码就可以使用并依赖于它们。
作为示例,编写一个提供餐厅功能的库crate。定义函数签名,但将函数体留空,只聚焦于代码的组织结构。
在餐饮业中,有前台front of house和后台back of house,前台负责招待顾客;包括一系列职责,后台则是厨师和烹饪人员在厨房工作、洗碗工清理餐具,以及经理处理行政事务的区域。
通过cargo new restaurant --lib来新建一个名为restaurant的库。下面是lib.rs中的前台部分:
rust
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模块下面有一个front_of_house模块,front_of_house模块下面有两个模块------hosting和serving,每个模块下面还包含一些函数。这样的结构叫做模块树。对于hosting和serving来说,它们互为兄弟。对于front_of_house和hosting来说,hosting是front_of_house的子模块,front_of_house是hosting的父模块。
3. 引用模块树中项的路径
为了向Rust指示在模块树中从何处查找某个项,需要使用路径。路径有两种形式:
1、绝对路径:以crate根开头的完整路径。对于外部crate代码来说,是以crate名称开头的绝对路径;对于当前crate的代码,则以字面值crate开头的绝对路径。
2、相对路径:从当前模块开始,以self、super或当前模块中的某个标识符开头。
绝对路径和相对路径都后跟一个或多个由双冒号分割的标识符。
回到之前restaurant的例子,我们在crate根定义了一个新函数eat_at_restaurant,并在其中希望调用add_to_waitlist函数。eat_at_restaurant是我们crate的一个公共API,使用pub关键字标记它。
rust
// lib.rs
mod front_of_house {
mod hosting {
fn add_to_waitlist() {}
}
}
pub fn eat_at_restaurant() {
// 绝对路径
crate::front_of_house::hosting::add_to_waitlist();
// 相对路径
front_of_house::hosting::add_to_waitlist();
}
eat_at_restaurant和add_to_waitlist定义在同一个crate中,可以使用crate关键字为起始的绝对路径。接着依次包含各级模块,直到找到add_to_waitlist。
eat_at_restaurant和front_of_house模块在同一层级,可以将front_of_house作为相对路径的起始,直到找到add_to_waitlist。
尝试编译上面的代码:

错误信息说明hosting模块是私有的,Rust不让我们使用hosting模块和其中的方法。在Rust中,所有项默认对父模块都是私有的。父模块中的项不能使用子模块中的私有项,但是子模块中的项可以使用父模块中的项。Rust选择以这种方式实现模块系统功能,因此默认隐藏内部实现细节。但是也提供了pub关键字来创建公共项,使子模块的内部部分暴露给上级模块。
3.1 通过pub关键字暴露路径
为了让父模块中的eat_at_restaurant函数可以访问子模块中的add_to_waitlist函数,可以使用pub关键字来标记hosting模块。
rust
mod front_of_house {
pub mod hosting {
fn add_to_waitlist() {}
}
}
// 省略
尝试构建:

&emsp在hosting前面加了pub,使其变成公有的。伴随着这种变化,如果我们可以访问front_of_house,那我们也可以访问hosting。但是hosting的内容仍是私有的,即使让模块公有,也不会使得其内容也是公有的。模块上的pub只允许父模块引用它,而不允许访问其中的代码。因为模块是一个容器,只是将模块变为公有能做的其实并不太多,同时还需要将模块中的一个或多个项变为公有。
私有性规则适用于所有项,之前学过的所有东西只要没有pub,就是私有的。
rust
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
// 省略
在绝对路径中,从crate开始,crate根中定义了front_of_house模块。front_of_house虽然没有公开,但是因为eat_at_restaurant函数与front_of_house位于同一级模块中,可以从eat_at_restaurant中引用front_of_house。hosting模块被pub标记了,我们可以访问hosting的父模块,自然可以访问hosting。add_to_waitlist被pub标记了,我们可以访问hosting,自然可以访问这个函数。
在相对路径中,其逻辑与绝对路径一样,除了第一步,不从crate根开始,路径从front_of_house开始。
3.2 二进制和库crate包的最佳实践
一个包可以同时包含一个二进制crate根和一个库crate根,两个crate默认以包名来命名。通常这种包,二进制crate只保留足以生成一个可执行文件的代码,由可执行文件调用库crate的代码。模块树定义在src/lib.rs中,公有项被二进制crate使用。
3.3 super开始的相对路径
可以通过在路径的开头使用super,从父模块开始构建相对路径,而不是从当前模块或者crate根开始。super指的是当前模块的父模块。
还是restaurant的例子,back_of_house模块定义的fix_incorrect_order函数通过指定的super(super在back_of_house中,back_of_house的父模块是crate,crate中有deliver_order函数)起始的deliver_order路径来调用父模块中的deliver_order函数。
rust
fn deliver_order() {}
mod back_of_house {
fn fix_incorrent_order() {
cook_order();
super::deliver_order();
}
fn cook_order() {}
}
fix_incorrect_order函数在back_of_house模块中,可以使用super进行back_of_house父模块,也就是crate根。在这里,我们可以找到deliver_order。
3.4 创建公有的结构体和枚举
可以使用pub设计公有的结构体和枚举。即使结构体是公有的,但是其字段仍是私有的。可以根据情况决定每个字段是否公有。
rust
mod back_of_house {
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() {
// 在夏天订购一个黑麦吐司作为早餐
let mut meal = back_of_house::Breakfast::summer("Rye");
// 改变主意更换想要面包的类型
meal.toast = String::from("Wheat");
println!("I'd like {} toast please", meal.toast);
// 如果不给下一行代码注释掉就不能编译
// 不允许查看或修改早餐附带的季节水果
meal.seasonal_fruit = String::from("blueberries");
}

对于结构体来说,被pub标记的字段可以访问修改,但是没有标记pub的字段无法访问修改。如果没有提供一个公共的关联函数来构造Breakfast的实例,那么在eat_at_restaurant中将无法创建Breakfast实例,因为不能设置私有字段seasonal_fruit的值。
如果将枚举设为公有,则它的所有变体都将变为公有。
rust
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;
}
将枚举类型声明为公有,其枚举变体都是公有的。但是如果枚举变体中包含数据,那么除非这个数据也是公有的,否则外部代码就无法构造、匹配和访问该变体中的数据。
4. 使用use关键字将路径引入作用域
在前面front_of_house和add_to_waitlist的例子中,每次通过绝对路径还是相对路径来调用add_to_waitlist,都很繁琐。幸运的是,可以使用use关键字创建一个捷径,然后就可以在作用域中使用更短的名字来调用这个函数了。
rust
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();
}
通过在crate根增加use crate::front_of_house::hosting,hosting在作用域中就是有效的名称了。use引入作用域的路径会检查私有性。use只能创建所在特定作用域内的捷径。
如果将eat_at_restaurant移动到一个叫customer的子模块中,这又是一个不同于use语句的作用域,函数不能编译。
rust
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
use front_of_house::hosting;
mod customer {
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
}

编译器显示捷径不再使用于customer模块中,同时use在其作用域内不在被使用,为了修复这个问题,可以将use移动到customer模块中变为use crate::front_of_house::hosting,或者在子模块customer内通过super::hosting引用父模块中的这个捷径。
4.1 创建惯用的use路径
上面的例子创建use路径没有直接创建到add_to_waitlist函数。这是因为如果将函数直接引入作用域,就无法清晰地表明函数是在哪里定义的。所以惯例做法是将函数所在的父模块引入作用域,这样既可以清晰地表明函数不是在本地定义的,又可以使路径重复度最小化。
使用use引入结构体、枚举和其他项时,习惯是执行它们的完整路径。
rust
use std::collections::HashMap;
fn main() {
let mut map = HashMap::new();
map.insert(1, 2);
}
Rust不允许通过use语句将两个具有相同名称的项带入作用域。
rust
use std::fmt;
use std::io;
fn function1() -> fmt::Result {
// 省略
}
fn function2() -> io::Result<()> {
// 省略
}
使用父模块可以区分这两个Result类型。如果指定use std::fmt::Result和use std::io::Result,在同一作用域中拥有了两个Result类型,当使用Result时,Rust不知道我们要用的是那个。
4.2 使用as关键字提供新的名称
使用use将两个同名类型引入同一作用域还有一个解决办法:在这个类型的路径后面,使用as指定一个新的本地名称或者别名。
rust
use std::fmt::Result;
use std::io::Result as IoResult;
fn function1() -> Result {
// 省略
}
fn function2() -> IoResult {
// 省略
}
4.3 使用pub use重导出名称
使用use关键字,将某个名称导入当前作用域后,该名称对此作用域之外还是私有的。若要让作用域之外的代码能够像在当前作用域中一样使用该名称,可以将pub和use组合使用,称为重导出。
rust
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();
}
在这个修改之前,外部代码需要使用路径restaurant::front_of_house::hosting::add_to_waitlist()来调用add_to_waitlist函数,并且还需要将front_of_house标记为pub。现在使用pub use从根模块导出了hosting模块,外部代码可以使用路径restaurant::hosting::add_to_waitlist。
4.4 使用外部包
在之前的猜猜看游戏中,项目使用了一个外部包rand来生成随机数。直接通过cargo add来增加依赖,Cargo会从crates.io下载rand和其依赖,并使其在项目中可用。之前通过use将RngExt trait引入作用域并调用了rand.rng函数。
std标准库对于你的包来说也是外部crate,只不过不需要通过cargo add增加依赖,可以通过use将标准库中定义的项引入项目包的作用域来引用它们。
4.5 使用嵌套路径来清理大量的use列表
当需要引入很多定义于相同包或相同模块的项,为每一项单独列出一行会占用源码大量的垂直空间。
rust
use std::cmp::Ordering;
use std::io;
我们可以使用嵌套路径将相同的项在一行中引入作用域。这么做需要指定路径的相同部分,接着是两个冒号,接着是大括号中的各自不同的路径部分。
rust
use std::{cmp::Ordering, io};
在较大的程序中,使用嵌套路径从相同的包或模块中引入很多项,可以显著减少所需的独立use语句的数量!
可以在路径的任何层级使用嵌套路径,这在组合两个共享子路径的use语句时非常有用。
rust
use std::io;
use std::io::Write;
两个路径的相同部分是std::io,这正是第一个路径。为了在一行use语句中引入这两个路径,可以在嵌套路径中使用self。
rust
use std::io::{self, Write};
4.6 使用glob运算符导入项
希望将一个路径下所有公有项引入作用域,可以指定路径后跟*glob运算符:
rust
use std::collections::*;
不太建议!glob运算符经常用于测试模块tests中,这是会将所有内容引入作用域。glob运算符有时也用于prelude模式中。
5. 将模块拆分成多个文件
前面的例子都在一个文件中定义了多个模块。当模块变大的时候,可能想将它们的定义移到单独的文件中,以便让代码更容易浏览。
还是餐厅的例子,我们将模块提取到各自的文件中,而不是将所有模块都定义到crate根文件中。
未拆分前:
rust
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
pub use front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
可以将front_of_house变为crate模块的子模块,hosting又是front_of_house的子模块。crate的子模块需要新建front_of_house.rs,在其中通过pub mod hosting;声明hosting子模块,hosting子模块必须放在front_of_house文件夹下,否则hosting模块就与front_of_house互为兄弟模块。必须新建front_of_house文件夹,在下面新建hosting.rs,在其中放入add_to_waitlist函数。
文件目录结构如下:

rust
// src/front_of_house/hosting.rs
pub fn add_to_waitlist() {}
rust
// src/front_of_house.rs
pub mod hosting;
rust
// src/lib.rs
mod front_of_house;
pub use front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
在模块树中,你只需要通过mod声明加载某个文件一次。一旦编译器知道这个文件是项目的一部分了,并且因为mod语句所在的位置而知道这段代码位于模块树的什么位置,那么项目中的其他文件就应该用它声明位置对应的路径来引用这段代码。mod不是include。