本文为阅读《Rust圣经》的第一篇笔记,内容包括《Rust基础入门》最后4章节(11~14),建议先看完本网站文章《Rust基础入门(一)/(二)/(三)》等,如果想查看原书请搜索“Rust圣经”或点击此链接:[Rust语言圣经(Rust Course)](https://course.rs/about-book.html)


包和模块

Rust 的组织管理

  • 项目(Package):一个 Cargo 提供的 feature ,可以用来构建、测试和分享包
  • 工作空间(WorkSpace):对于大型项目,可以进一步将多个包联合在一起,组织成工作空间
  • 包(Crate):一个由多个模块组成的树形结构,可以作为三方库进行开发,也可以生成可执行文件进行运行
  • 模块(Module):可以一个文件多个模块,也可以一个文件一个模块,模块可以被认为是真实项目中的代码组织单元

包和项目

包 Crate

对于 Rust 而言,包是一个独立的可编译单元,它编译后会生成一个可执行文件或者一个库。一个包会将相关联的功能打包在一起,使得该功能可以很方便的在多个项目中分享。同一个包中不能有同名的类型,但是在不同包中就可以。

项目 Package

由于 Package 就是一个项目,因此它包含有独立的 Cargo.toml 文件,以及因为功能性被组织在一起的一个或多个包。一个 Package 只能包含一个库(library)类型的包,但是可以包含多个二进制可执行类型的包。

  • 二进制 Package

    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 run 可以运行该项目,输出 Hello, world!

  • 库 Package

    1
    2
    3
    4
    5
    6
    7
    $ cargo new my-lib --lib
    Created library `my-lib` package
    $ ls my-lib
    Cargo.toml
    src
    $ ls my-lib/src
    lib.rs

    这里使用 cargo run 不能运行 my-lib ,因为库类型的 Package 只能作为三方库被其它项目引用,而不能独立运行,只有之前的二进制 Package 才可以运行。

理解:项目(Package)是包含 Cargo.toml的工程目录,包(Crate)是项目内部的编译单元(每个 src/main.rs或 src/lib.rs定义一个包)。

一个真实项目中典型的 Package,会包含多个二进制包,这些包文件被放在 src/bin 目录下,每一个文件都是独立的二进制包,同时也会包含一个库包,该包只能存在一个 src/lib.rs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.
├── Cargo.toml
├── Cargo.lock
├── src
│ ├── main.rs
│ ├── lib.rs
│ └── bin
│ └── main1.rs
│ └── main2.rs
├── tests
│ └── some_integration_tests.rs
├── benches
│ └── simple_bench.rs
└── examples
└── simple_example.rs
  • 唯一库包:src/lib.rs
  • 默认二进制包:src/main.rs,编译后生成的可执行文件与 Package 同名
  • 其余二进制包:src/bin/main1.rs 和 src/bin/main2.rs,它们会分别生成一个文件同名的二进制可执行文件
  • 集成测试文件:tests 目录下
  • 基准性能测试 benchmark 文件:benches 目录下
  • 项目示例:examples 目录

模块 Module

模块的使用

使用模块可以将包中的代码按照功能性进行重组,最终实现更好的可读性及易用性。

使用 cargo new –lib restaurant 创建一个小餐馆,注意,这里创建的是一个库类型的 Package,然后将以下代码放入 src/lib.rs 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 餐厅前厅,用于吃饭
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 关键字来创建新模块,后面紧跟着模块名称
  • 模块可以嵌套,这里嵌套的原因是招待客人和服务都发生在前厅,因此我们的代码模拟了真实场景
  • 模块中可以定义各种 Rust 类型,例如函数、结构体、枚举、特征等
  • 所有模块均定义在同一个文件中

模块树

src/main.rssrc/lib.rs 被称为包根(crate root),因为这两个文件的内容形成了一个模块 crate,该模块位于包的树形结构(由模块组成的树形结构)的根部:

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

这颗树展示了模块之间彼此的嵌套关系,因此被称为模块树。其中 crate 包根是 src/lib.rs 文件,包根文件中的三个模块分别形成了模块树的剩余部分。该树结点间的父/子结点关系被称为父/子模块

用路径引用模块

想要调用一个函数,就需要知道它的路径,在 Rust 中,这种路径有两种形式:

  • 绝对路径,从包根开始,路径名以包名或者 crate 作为开头
  • 相对路径,从当前模块开始,以 self,super 或当前模块的标识符作为开头

举个例子:

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
mod front_of_house {
mod hosting {
fn add_to_waitlist() {}
}
}

pub fn eat_at_restaurant() {
// 绝对路径
crate::front_of_house::hosting::add_to_waitlist();

// 相对路径(self::可省略)
self::front_of_house::hosting::add_to_waitlist();
}

fn serve_order() {}

// 厨房模块
mod back_of_house {
fn fix_incorrect_order() {
cook_order();

// 相对路径(super代表的是父模块为开始的引用方式)
super::serve_order();
}

fn cook_order() {}
}

  • 绝对路径引用:因为 eat_at_restaurant 和 add_to_waitlist 都定义在一个包中,因此在绝对路径引用时,可以直接以 crate 开头,然后逐层引用,每一层之间使用 :: 分隔
  • 相对路径引用:因为 eat_at_restaurant 和 front_of_house 都处于包根 crate 中,因此相对路径可以使用 front_of_house 作为开头

代码可见性

Rust 的模块可见性规则总结如下:

  1. 默认私有原则:所有模块、函数、结构体、枚举、常量等默认都是私有的
  2. pub关键字:用于控制项对外的可见性
  3. 可见性规则: 父模块无法访问子模块的私有项 子模块可以访问祖先模块的所有项 模块可见性不等于其内部项的可见性
  4. 层级控制:需要对外暴露的每一层级都需要显式添加 pub
  5. 路径访问:通过绝对路径(crate::)或相对路径访问时,路径上的每个部分都必须可见
  6. 模块对外可见后,其内部的函数、结构体等还需要单独标记 pub才能被外部访问。

结构体和枚举的可见性

结构体和枚举的可见性:

  • 将结构体设置为 pub,但它的所有字段依然是私有的
  • 将枚举设置为 pub,它的所有字段也将对外可见

使用 use 及受限可见性

使用 use

在 Rust 中,可以使用 use 关键字把路径提前引入到当前作用域中,随后的调用就可以省略该路径,极大地简化了代码。

模块与文件分离

按照之前格式,代码应该如下:

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

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

为了将模块放入一个单独文件便于管理,可以进行如下修改:

把 front_of_house 前厅分离出来,放入一个单独的文件中 src/front_of_house.rs:

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

然后,将以下代码留在 src/lib.rs 中:

1
2
3
4
5
6
7
8
9
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 从另一个和模块 front_of_house 同名的文件中加载该模块的内容
  • 使用绝对路径的方式来引用 hosting 模块:crate::front_of_house::hosting;
  • 模块 front_of_house 的定义还是在 src/lib.rs 中,只不过模块的具体内容被移动到了 src/front_of_house.rs 文件中。

上述代码会报错,因为如果需要将文件夹作为一个模块,我们需要进行显示指定暴露哪些子模块。

以下为两种修改方法:

第一种方法:创建 mod.rs 文件

文件结构:

1
2
3
4
5
src/
├── front_of_house/
│ ├── hosting.rs
│ └── mod.rs
└── lib.rs

文件内容:

  • src/lib.rs:
1
2
3
4
5
6
7
8
9
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/mod.rs:
1
pub mod hosting;
  • src/front_of_house/hosting.rs:
1
pub fn add_to_waitlist() {}

第二种方法:创建同名的 rs 文件

文件结构:

1
2
3
4
5
src/
├── front_of_house/
│ └── hosting.rs
├── front_of_house.rs
└── lib.rs

文件内容:

  • src/lib.rs:
1
2
3
4
5
6
7
8
9
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:
1
pub mod hosting;
  • src/front_of_house/hosting.rs:
1
pub fn add_to_waitlist() {}
  1. 第一种方法(传统方法): 在 front_of_house目录中创建 mod.rs文件 这个文件声明了该模块包含的子模块 适用于所有 Rust 版本
  2. 第二种方法(新风格): 在与 front_of_house目录同级的位置创建 front_of_house.rs文件 这个文件声明了该模块包含的子模块 从 Rust 1.30 开始支持 优势:避免项目中出现大量同名的 mod.rs文件,提高代码导航的便利性

无论使用哪种方法,编译都能成功,程序可以正常运行。

基本引入方式

绝对路径引入模块

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;

pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
hosting::add_to_waitlist();
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 front_of_house::hosting::add_to_waitlist;

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

引入函数的方式

1
2
3
4
5
6
use std::collections::HashMap;

fn main() {
let mut map = HashMap::new();
map.insert(1, 2);
}

其实严格来说,对于引用方式并没有需要遵守的惯例,建议:优先使用最细粒度(引入函数、结构体等)的引用方式,如果引起了某种麻烦(例如前面两种情况),再使用引入模块的方式。

避免同名引用

  • 使用父模块的方式来调用
1
2
3
4
5
6
7
8
9
10
use std::fmt;
use std::io;

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

fn function2() -> io::Result<()> {
// --snip--
}
  • 使用 as 关键字
1
2
3
4
5
6
7
8
9
10
use std::fmt::Result;
use std::io::Result as IoResult;

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

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

引入项再导出

(感觉这个挺奇怪的www,可能还不太理解吧)

当外部的模块项 A 被引入到当前模块中时,它的可见性自动被设置为私有的,如果你希望允许其它外部代码引用我们的模块项 A,那么可以对它进行再导出:

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

如上,使用 pub use 即可实现。这里 use 代表引入 hosting 模块到当前作用域,pub 表示将该引入的内容再度设置为可见。

当你希望将内部的实现细节隐藏起来或者按照某个目的组织代码时,可以使用 pub use 再导出,例如统一使用一个模块来提供对外的 API,那该模块就可以引入其它模块中的 API,然后进行再导出,最终对于用户来说,所有的 API 都是由一个模块统一提供的。

使用第三方包

1
2
3
4
5
use rand::Rng;

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

Rust 社区已经为我们贡献了大量高质量的第三方包,你可以在 crates.io 或者 lib.rs 中检索和使用,从目前来说查找包更推荐 lib.rs,搜索功能更强大,内容展示也更加合理,但是下载依赖包还是得用crates.io。

使用 {} 简化引入方式

1
2
3
4
5
6
7
8
9
10
use std::collections::HashMap;
use std::collections::BTreeMap;
use std::collections::HashSet;

use std::cmp::Ordering;
use std::io;

// 等价于
use std::collections::{HashMap,BTreeMap,HashSet};
use std::{cmp::Ordering, io};
1
2
3
4
5
use std::io;
use std::io::Write;

// 等价于
use std::io::{self, Write};
  • use self::xxx,表示加载当前模块中的 xxx。此时 self 可省略
  • use xxx::{self, yyy},表示,加载当前路径下模块 xxx 本身,以及模块 xxx 下的 yyy

使用 * 引入模块下的所有项

1
use std::collections::*;

以上这种方式来引入 std::collections 模块下的所有公共项,这些公共项自然包含了 HashMap,HashSet 等想手动引入的集合类型。

当使用 * 来引入的时候要格外小心,因为你很难知道到底哪些被引入到了当前作用域中,有哪些会和你自己程序中的名称相冲突:

1
2
3
4
5
6
7
use std::collections::*;

struct HashMap;
fn main() {
let mut v = HashMap::new();
v.insert("a", 1);
}

以上代码中,std::collections::HashMap 被 * 引入到当前作用域,但是由于存在另一个同名的结构体,因此 HashMap::new 根本不存在,因为对于编译器来说,本地同名类型的优先级更高。

受限可见性

  • pub- 完全公开
1
pub fn public_function() {}  // 任何地方都可访问
  • pub(crate)- 当前包内可见
1
pub(crate) fn crate_only() {}  // 只在当前 crate 内可见
  • pub(super)- 父模块可见
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mod parent {
mod child {
pub(super) fn parent_visible() {} // 只在父模块 parent 中可见
}

fn test() {
child::parent_visible(); // ✅ 可以访问
}
}

mod other {
fn test() {
// parent::child::parent_visible(); // ❌ 不能访问
}
}
  • pub(self)- 当前模块可见
1
2
3
4
5
6
7
8
9
10
11
12
13
14
mod my_module {
pub(self) fn only_here() {} // 等价于不写 pub

fn test() {
only_here(); // ✅ 可以访问
}
}

// other_module.rs
mod other_module {
fn test() {
// my_module::only_here(); // ❌ 不能访问
}
}
  • pub(in <path>)- 指定路径可见
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
mod a {
pub(in crate::a) fn only_in_a() {} // 只在模块 a 中可见

mod b {
pub(in crate::a) fn also_in_a() {} // 只在模块 a 中可见

fn test() {
also_in_a(); // ✅ 可以访问
}
}

fn test() {
only_in_a(); // ✅ 可以访问
b::also_in_a(); // ✅ 可以访问
}
}

mod c {
fn test() {
// a::only_in_a(); // ❌ 不能访问
// a::b::also_in_a(); // ❌ 不能访问
}
}