❝所有系统都有一种自毁趋势,往“熄灭”或者“圆寂”方向发展。这个趋势就叫“熵增”。为了维持系统,需要持续的输入能量,这种持续输入的能量我们就叫“负熵流”。 《向上生长》❞
大家好,我是「柒八九」。
今天,我们继续「Rust学习笔记」的探索。我们来谈谈关于「包、Crate和模块」的相关知识点。
如果,想了解该系列的文章,可以参考我们已经发布的文章。如下是往期文章。
文章list
- Rust学习笔记之Rust环境配置和入门指南
- Rust学习笔记之基础概念
- Rust学习笔记之所有权
- Rust学习笔记之结构体
- Rust学习笔记之枚举和匹配模式
你能所学到的知识点
❝
Rust
中包和crate
「推荐阅读指数」 ⭐️⭐️⭐️⭐️⭐️模块
控制作用域与私有性 「推荐阅读指数」 ⭐️⭐️⭐️⭐️路径
用于引用模块树中的项 「推荐阅读指数」 ⭐️⭐️⭐️⭐️use
将名称
引入作用域 「推荐阅读指数」 ⭐️⭐️⭐️⭐️- 将模块分割进不同文件 「推荐阅读指数」 ⭐️⭐️⭐️❞
好了,天不早了,干点正事哇。
伴随着项目的增长,你可以通过将代码分解为多个模块和多个文件来组织代码。「一个包可以包含多个二进制 crate
项和一个可选的 crate
库」。伴随着包的增长,你可以将包中的部分代码提取出来,做成独立的 crate
,这些 crate
则作为「外部依赖项」。
❝
Rust
的模块系统the module system,包括:
- 包(
Packages
):Cargo
的一个功能,它允许你构建、测试和分享crate
。 Crates
:一个「模块的树形结构」,它形成了库或二进制项目。- 模块(
Modules
)和use
:允许你控制作用域和路径的私有性。 - 路径(
path
):一个命名例如结构体、函数或模块等项的方式❞
包和 crate
- 包(
package
) 是「提供一系列功能的一个或者多个crate
。」一个包会包含有一个Cargo.toml
文件,阐述如何去构建这些crate
。 crate
是一个「二进制项或者库」。crate root
是一个「源文件」,Rust
编译器以它为起始点,并构成你的crate
的根模块。
❝包中所包含的内容由几条规则来确立。
- 一个包中「至多只能」包含一个库 cratelibrary crate;
- 包中可以包含「任意多」个二进制 cratebinary crate;
- 包中「至少包含」一个
crate
,无论是库的还是二进制的。❞
输入命令 cargo new
:
$ 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
就是一个「与包同名」的二进制 cratebinary crate 的crate
根。- 同样的,
Cargo
知道如果包目录中包含src/lib.rs
,则包带有与其同名的库 cratelibrary crate,且src/lib.rs
是crate
根。❞
crate
根文件将由 Cargo
传递给 rustc
来实际构建库或者二进制项目。
如果一个包「同时含有」 src/main.rs
和 src/lib.rs
,则它有两个 crate
:「一个库和一个二进制项,且名字都与包相同」。
❝通过将文件放在
src/bin
目录下,一个包可以拥有「多个二进制」crate
:每个src/bin
下的文件都会被编译成一个独立的二进制 cratebinary crate。 ❞
一个 crate
会将一个作用域内的「相关功能分组到一起」,使得该功能可以很方便地在多个项目之间共享
。
定义模块来控制作用域与私有性
「模块」 让我们可以将一个 crate
中的「代码进行分组,以提高可读性与重用性」。模块还可以控制项的 「私有性」
项
是可以被外部代码
使用的(public
)- 还是作为一个
内部实现
的内容,不能被外部代码使用(private
)。
通过执行 cargo new --lib restaurant
,来创建一个新的名为 restaurant的库
。在 restaurant/src/lib.rs
中,来定义一些模块和函数。
fn main() {
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() {}
}
}
}
用「关键字 mod
定义一个模块」,指定模块的名字,并用大括号包围模块的主体。我们可以在模块中包含其他模块,就像本示例中的 hosting
和 serving
模块。模块中也可以包含其他项,比如结构体、枚举、常量、trait
。
通过使用模块,我们可以把「相关的定义组织起来」,并通过模块命名来解释为什么它们之间有相关性。使用这部分代码的开发者可以更方便的循着这种分组找到自己需要的定义,而不需要通览所有。编写这部分代码的开发者通过分组知道该把新功能放在哪里以便继续让程序保持组织性。
之前我们提到,src/main.rs
和 src/lib.rs
被称为 crate 根
。如此称呼的原因是,这两个文件中「任意一个的内容会构成名为 crate
的模块」,且该模块位于 crate
的被称为 模块树 的模块结构的根部at the root of the crate’s module structure。
上面的代码所对应的模块树
如下所示。
crate
└── front_of_house
├── hosting
│ ├── add_to_waitlist
│ └── seat_at_table
└── serving
├── take_order
├── serve_order
└── take_payment
这个树展示了模块间是如何相互嵌套的。这个树还展示了一些模块互为「兄弟」 ,即它们被定义在同一模块内。
路径用于引用模块树中的项
在 Rust
使用路径
的方式在模块树中找到一个项
的位置,就像在文件系统使用路径一样。如果我们想要调用一个函数,我们需要知道它的路径。
❝路径有两种形式:
- 绝对路径absolute path从
crate
根部开始,以crate
名或者字面量crate
开头。 - 相对路径relative path从「当前模块开始」,以
self
、super
或当前模块的标识符开头。❞
绝对路径
和相对路径
都后跟一个或多个由双冒号(::
)分割的标识符。
在 crate
根部定义了一个新函数 eat_at_restaurant
,并在其中展示调用 add_to_waitlist
函数的两种方法。eat_at_restaurant
函数是我们 crate 库
的一个「公共 API
,所以我们使用 pub
关键字来标记它」。
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
函数,使用的是「绝对路径」。add_to_waitlist
函数与 eat_at_restaurant
被定义在「同一 crate 中」,这意味着我们可以「使用 crate
关键字为起始的绝对路径」。
在 crate
后面,我们持续地嵌入模块,直到我们找到 add_to_waitlist
。你可以想象出一个相同结构的文件系统,我们通过指定路径 /front_of_house/hosting/add_to_waitlist
来执行 add_to_waitlist
程序。我们使用 crate
从 crate
根部开始就类似于在 shell
中使用 /
从文件系统根开始。
第二种方式,我们在 eat_at_restaurant
中调用 add_to_waitlist
,使用的是「相对路径」。这个路径以 front_of_house
为起始,这个模块在模块树中,与 eat_at_restaurant
定义在「同一层级」。与之等价的文件系统路径就是 front_of_house/hosting/add_to_waitlist
。以名称为起始,意味着该路径是相对路径。
模块不仅对于你组织代码很有用。他们还定义了 Rust
的 私有性边界privacy boundary:这条界线不允许外部代码了解、调用和依赖被封装的实现细节。所以,如果「你希望创建一个私有函数或结构体,你可以将其放入模块」。
❝
Rust
中「默认所有项」(函数、方法、结构体、枚举、模块和常量)都是私有的。
- 「父模块中的项不能使用子模块中的私有项,但是子模块中的项可以使用他们父模块中的项」。 这是因为子模块封装并隐藏了他们的实现详情,但是「子模块可以看到他们定义的上下文」。❞
使用 pub 关键字暴露路径
想让父模块中的 eat_at_restaurant
函数可以访问子模块中的 add_to_waitlist
函数,因此我们使用 pub
关键字来标记 hosting
模块
mod front_of_house {
pub 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();
}
cargo build
的时候,代码编译仍然有错误。
在 mod
hosting
前添加了 pub
关键字,使其变成公有的。伴随着这种变化,如果我们可以访问 front_of_house
,那我们也可以访问 hosting
。但是 hosting
的 内容contents仍然是私有的;
❝这表明「使模块公有并不使其内容也是公有的。模块上的
pub
关键字只允许其父模块引用它」。❞
继续将 pub
关键字放置在 add_to_waitlist
函数的定义之前,使其变成公有。
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
pub fn eat_at_restaurant() {
// 绝对路径
crate::front_of_house::hosting::add_to_waitlist();
// 相对路径
front_of_house::hosting::add_to_waitlist();
}
使用 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
。成功!
创建公有的结构体和枚举
还可以使用 pub
来设计公有的结构体
和枚举
,不过有一些额外的细节需要注意。如果我们在一个结构体定义
的前面使用了 pub
,这个结构体会变成公有的,但是这个「结构体的字段仍然是私有的」。我们可以「根据情况决定每个字段是否公有」。
定义了一个公有结构体 back_of_house:Breakfast
,其中有一个公有字段 toast
和私有字段 seasonal_fruit
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("桃子"),
}
}
}
}
pub fn eat_at_restaurant() {
let mut meal = back_of_house::Breakfast::summer("Rye");
meal.toast = String::from("Wheat");
println!("我喜欢吃{} ", meal.toast);
}
因为 back_of_house::Breakfast
结构体的 toast
字段是公有的,所以我们可以在 eat_at_restaurant
中使用点号来随意的读写 toast
字段。
因为 back_of_house::Breakfast
「具有私有字段」,所以这个「结构体需要提供一个公共的关联函数来构造 Breakfast 的实例」。如果 Breakfast
没有这样的函数,我们将无法在 eat_at_restaurant
中创建 Breakfast
实例,因为我们不能在 eat_at_restaurant
中设置私有字段 seasonal_fruit
的值。
代码语言:javascript复制❝如果我们将枚举设为公有,则它的「所有成员都将变为公有」。我们只需要在
enum
关键字前面加上pub
。 ❞
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 关键字将名称引入作用域
我们可以使用 use
关键字「将路径
一次性引入作用域」,然后调用该路径
中的项,就如同它们是本地项一样。
将 crate::front_of_house::hosting
模块引入了 eat_at_restaurant
函数的作用域,而我们只需要指定 hosting::add_to_waitlist
即可在 eat_at_restaurant
中调用 add_to_waitlist
函数。
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();
hosting::add_to_waitlist();
hosting::add_to_waitlist();
}
fn main() {}
在作用域中增加 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();
}
fn main() {}
创建惯用的 use 路径
要想使用 use
将函数的「父模块引入作用域」,我们必须在调用函数时指定父模块,这样可以清晰地表明函数不是在本地定义的,同时使完整路径的重复度最小化。
另一方面,使用 use
引入结构体、枚举和其他项时,习惯是「指定它们的完整路径」。
use std::collections::HashMap;
fn main() {
let mut map = HashMap::new();
map.insert(1, 2);
}
这个习惯用法有一个例外,那就是我们想使用 use
语句将两个「具有相同名称的项」带入作用域,因为 Rust
不允许这样做。
fn main() {
use std::fmt;
use std::io;
fn function1() -> fmt::Result {
// --snip--
Ok(())
}
fn function2() -> io::Result<()> {
// --snip--
Ok(())
}
}
使用「父模块」可以区分这两个 Result
类型。如果我们是指定 use std::fmt::Result
和 use std::io::Result
,我们将在同一作用域拥有了两个 Result
类型,当我们使用 Result
时,Rust
则不知道我们要用的是哪个。
使用 as 关键字提供新的名称
使用 use
将两个「同名类型引入同一作用域」这个问题还有另一个解决办法:在这个类型的路径后面,我们使用 as
指定一个「新的本地名称或者别名」。
fn main() {
use std::fmt::Result;
use std::io::Result as IoResult;
fn function1() -> Result {
// --snip--
Ok(())
}
fn function2() -> IoResult<()> {
// --snip--
Ok(())
}
}
在第二个 use
语句中,我们选择 IoResult
作为 std::io::Result
的新名称,它与从 std::fmt
引入作用域的 Result
并不冲突。
使用 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;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
hosting::add_to_waitlist();
hosting::add_to_waitlist();
}
fn main() {}
通过 pub use
,现在可以通过新路径 hosting::add_to_waitlist
来调用 add_to_waitlist
函数。如果没有指定 pub use
,eat_at_restaurant
函数可以在其作用域中调用 hosting::add_to_waitlist
,但「外部代码则不允许使用这个新路径」。
使用外部包
假设项目使用了一个外部包
,rand
,来生成随机数。为了在项目中使用 rand
,在 Cargo.toml
中加入了如下行:
文件名: Cargo.toml
[dependencies]
rand = "0.8.3"
在 Cargo.toml
中加入 rand
依赖告诉了 Cargo
要从 crates.io
下载 rand
和其依赖,并使其可在项目代码中使用。
接着,为了将 rand
定义引入项目包的作用域
,我们加入一行 use
起始的包名,它以 rand
包名开头并列出了需要引入作用域的项
。
use rand::Rng;
fn main() {
let secret_number = rand::thread_rng()
.gen_range(1..101);
}
❝
crates.io
上有很多Rust
社区成员发布的包,将其引入你自己的项目都需要一道相同的步骤:在Cargo.toml
列出它们并通过use
将其中定义的项
引入项目包的作用域中。 ❞
标准库(std
)对于你的包来说也是外部 crate
。因为标准库随 Rust
语言一同分发,无需修改 Cargo.toml
来引入 std
,不过需要通过 use
将标准库中定义的项
引入项目包的作用域中来引用它们,比如我们使用的 HashMap
:
fn main() {
use std::collections::HashMap;
}
嵌套路径来消除大量的 use 行
当需要引入很多定义于相同包或相同模块的项
时,为每一项单独列出一行会占用源码很大的空间。
fn main() {
use std::cmp::Ordering;
use std::io;
}
我们可以使用「嵌套路径」将相同的项
在一行中引入作用域。这么做需要「指定路径的相同部分,接着是两个冒号,接着是大括号中的各自不同的路径部分」。
fn main() {
use std::{cmp::Ordering, io};
// ---snip---
}
通过 glob 运算符将所有的公有定义引入作用域
如果希望将一个路径下 所有「公有项」引入作用域,可以指定路径后跟 *
:
fn main() {
use std::collections::*;
}
这个 use
语句将 std::collections
中定义的所有公有项
引入当前作用域。
将模块分割进不同文件
当模块变得更大时,你可能想要将它们的定义移动到单独的文件中,从而使代码更容易阅读。
将 front_of_house
模块移动到属于它自己的文件 src/front_of_house.rs
中,通过改变 crate
根文件。在这个例子中,crate
根文件是 src/lib.rs
,这也同样适用于以 src/main.rs
为 crate
根文件的二进制 crate
项。
文件名: src/lib.rs
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();
}
文件名: src/front_of_house.rs
pub mod hosting {
pub fn add_to_waitlist() {}
}
在 mod front_of_house
后「使用分号」,而不是代码块,这将告诉 Rust
在另一个与模块同名的文件中加载模块的内容。