【Rust 学习记录】7. 包、单元包和模块

TwoSix Lv3

包与单元包

  1. 单元包(Crate):单元包可以被用来生成二进制的程序或者库;单元包的入口文件称为单元包的入口节点
  2. 包(Package):一个包由一个或多个单元包集合而成,用 Cargo.toml 描述包内的单元包怎么构建;一个包最多也拥有一个库单元包;包可以有多个二进制单元包;包必须至少拥有一个单元包,可以是库单元包,也可以是二进制单元包。

举个例子,我们一直用的cargo new test命令就是用来生成一个名为 test 包的,其中我们的代码文件 src/main.rs 就是 test 包下的一个单元包,叫 main,代码文件就是这个单元包的根节点,这类代码编译后可以生成一个二进制可执行文件,所以也叫二进制单元包。我们也可以通过不断的在 src 目录下写代码,创建更多的二进制单元包。

书上并没有介绍我更关心的库单元包,只是它举了个文件名例子src/lib.rs。后面再看。

同时,每个单元包内的代码都有自己的作用域(和 c++ 的命名空间相当),用来避免命名冲突。也就是我们之前 use 了 rand 包后,需要指定rand::Rng才能使用Rng模块一样。这样可以避免 Rng 这个名字和你自己定义的 Rng 结构冲突,你可以更随心所欲的命名。

模块

模块可以提供私有/公共的权限管理功能,也就是熟知的 private 和 public

模块的定义

我们现在 src 文件夹下创建一个lib.rs文件,新建一个库单元包,然后再编写代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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(){

}
}
}

代码解释

  1. mod关键字:mod 关键字用来定义一个模块,我们定义了一个名为 front_of_house 的模块,用来管理餐厅的前厅部分,并且前厅部分又分为服务客户的服务员,处理订单的前台等,所以我们在模块下又定义了两个模块 hosting, serving
  2. 接着我们以模块来对代码进行分组,在不同模块下定义了对应的功能函数

这段代码就相当于我们构建了一个单元包,单元包里包含了一个模块,模块内有两个模块,而两个模块又有多个函数,构成了一个树的层级结构。

包管理架构

模块的调用

我们可以通过绝对路径和相对路径的方式来调用这个层级结构的某个函数。

Rust 通过 :: 符号来构建访问路径,当我们想调用 hostting模块下的 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
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(){

}
}
}

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

front_of_house::hosting::add_to_waitlist();
}
  1. 绝对路径:crate::front_of_house::hosting::add_to_waitlist(); 从指定的入口文件开始访问到函数
  2. 相对路径:front_of_house::hosting::add_to_waitlist(); 用相对路径来访问当前函数同级下的函数,这里的eat_at_restaurantfront_of_house同级

绝对路径和相对路径的优缺点也不用多说了吧,当可能需要同步移动两个模块的时候,相对路径好,单独移动一个模块的时候用绝对路径好。视情况而定就好

访问权限

刚说了上面的代码是编译不通过的,我们编译一下代码,看看为什么不通过,

1
error[E0603]: module `hosting` is private

报错,hosting 是私有的

也就是说,Rust 里所有的条目,在没有特意声明的情况下,默认都是私有的,权限这块的规则大概是:父级模块无法访问私有的子模块条目,子模块可以访问父级模块的私有条目,同级之间可以互相访问。

但是注意,公有的模块不代表模块里的字段公有。例如

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

}
fn seat_at_table(){

}
}

mod serving{
fn take_order(){

}
fn serve_order(){

}
fn take_payment(){

}
}
}

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

front_of_house::hosting::add_to_waitlist();
}

这里我们用pub关键字声明了hosting模块公有,但实际仍然会编译错误。因为这一次声明,只是声明了父级的 front_of_house 模块可以访问 hosting 模块了,但父级 hosting 模块却依旧不能访问它的私有字段 add_to_waitlist

也就是说,父级条目是不是公有的并不影响它内部的条目的公有或私有状态

所以要代码能编译通过很简单,我们把 add_to_waitlist 也公开就行。

可能有人想问,为什么 front_of_house 没有公开也能访问?因为 front_of_house 和 eat_at_restaurant 是同级的,具有互相访问的权限。

super 关键字

super 关键字和python一样,用来查找到父模块的路径,属于相对路径的一种。应该不用多说,用一个代码来当例子吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
mod front_of_house{
pub mod hosting{
pub fn add_to_waitlist(){

}
fn seat_at_table(){

}
}

mod serving{
fn take_order(){
super::hosting::add_to_waitlist(); // 通过相对路径访问到add_to_waitlist
}
fn serve_order(){

}
fn take_payment(){

}
}
}

结构体和枚举类型的公有

结构体

结构体的权限和模块基本相同,也就是父级模块的公有,不影响子字段的公有或私有

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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);
println!("I'd like {} fruit please", meal.seasonal_fruit);
}

这里,我们首先定义了一个后厨模块 back_of_house,定义了一个早餐结构体 Breakfast,包含吐司和水果两个变量,一个公有一个私有,然后实现了一个结构体内的函数 summer,用来创建一个包含指定吐司和水果 Breakfast 结构体。

后续,我们在 eat_at_restaurant通过相对路径创建了一个可变的 Breakfast 结构体,并试图访问结构体的字段。

通过观察报错就可以知道,公有的 meal.toast 可以正常的访问和修改,但私有的 meal.seasonal_fruit 无法访问,也就无从谈起修改了。

值得一提的是,因为 Breakfast 有一个私有字段,所以如果我们不定义一个子级的 summer 函数,我们甚至不能创建一个 Breakfast 结构体,因为我们没办法在外部给 seasonal_fruit 赋值。

枚举类型

但枚举类型不一样,枚举公有后,所有字段都公有了,因为枚举类型这东西必须全公有才好用,一个公有一个私有,没有什么意义,例如说你 match 总要做个完整性检查吧?字段都不能全部完全访问,何谈完整的处理?所以一半私有一半公有是没有意义的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 enum Appetizer{
Soup,
Salad,
}
}

pub fn eat_at_restaurant() {
let a = back_of_house::Appetizer::Soup;
let b = back_of_house::Appetizer::Salad;
}

这里我们新加了一个枚举类型前菜 Appetizer,字段没有声明为公有,但依旧可以正常访问。

use关键字

use的基本使用

如果我们要多次调用模块里的函数,如果一直要写一大串的路径似乎有点麻烦。use关键字就是用来简化这个步骤的。原理就叫:引入作用域

1
2
3
4
5
6
// 还是前面的代码,前厅部分,为了美观省略了
use crate::front_of_house::hosting;

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

这里我们就用 use 关键字,用绝对路径的方法把 hosting 这个字段引入了当前作用域,这样 hosting 就可以作为本地的字段,直接使用就行了,当然,这样引入后自然也会和本地的 hosting 字段冲突,你不能再定义一个叫 hosting 的东西了。

或许有人想问 use 相对路径行不?可以,可以自己试试。

PS:在书上的版本还需要在相对路径前加入 self 字段来指定当前所在的作用域,但它提到了有些开发者在视图去掉这个字段。我的版本下没加也编译通过了,看来他们成功了。

后面书上还提到了一个代码规范,虽然我不会这么写,但似乎 csdn 的博客上有不少人确实会这样喜欢省事,我简单拉出来提一下。

有人可能会问,上面的代码为什么只 use 到了 hosting,既然只用 add_to_waitlist 函数为什么不直接 use 到 add_to_waitlist 函数呢?

原因很简单,我们需要告诉所有看这段代码的人,当然也包括自己,这个函数并不是在本地定义的,而是引用了哪里的一个包/模块里面的函数,既增加了一定的可读性,也防止了一部分同名。毕竟很多最底层的字段命名,一般都是十分通用的名字。这是一个好习惯。

pub use

use 字段通常是以私有的方式,引入当前作用域,也就是说,你引入之后,也只是在当前 use 的作用域生效,在其地方是不生效的,依旧要通过路径访问。pub use 字段就是解决这个问题的,让其他代码也能简单的导入代码。

这个的应用场景主要是,你为了自己更方便的管理自己的代码,所以分了很多层级结构,但其实其他代码并不是很在乎你这块代码的很多结构,就比如餐厅的例子,顾客不需要在乎你的前厅,你的后厨,顾客只在乎来餐厅吃饭的几个步骤,坐座位,点单,上菜,吃。所以你就可以使用 pub use,把这几个步骤从前厅后厨里导出来,方便顾客使用。

例子到后面跨文件的部分再举吧。

嵌套路径use

当我们使用包内的多个模块的时候,每个都写一行 use 会占用很多地方,这时候就可以用嵌套路径的方法。用标准包为例子

1
2
3
4
5
use std::cmp::Ordering;
use std::io;

// 下面的写法和上面等价
use std::{cmp::Ordering, io};

也就是用花括号把同级的模块括起来一起导入,逗号分开。

如果我导入了一个模块,又想导入模块下面的一个函数呢?可以用 self 代表当前路径

1
2
3
4
5
use std::io;
use std::io::Write;

// 下面的写法和上面等价
use std::{self, Write};

如果我想导入一个模块的所有字段的?可以用统配符*,就相当于 import * 吧。是个坏文明,因为这样你就不知道你写的字段是不是和包内的字段重名了,别用。

1
use std::io::*;

将模块拆分为不同文件

之前说过了模块的层级目录对吧,我们可以把这个层级目录转换为对应的文件层级目录,实现不同文件对应不同模块。

用之前写的代码 front_of_house->hosting->add_to_waitlist 为例子。

首先,我们需要在我们的库单元包声明一个前台模块。

lib.rs

1
2
3
4
5
6
7
mod front_of_house;

pub use crate::front_of_house::hosting;

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

然后,对应的再在 lib.rs 的同级目录下新建一个 front_of_house.rs 文件,对应模块的入口。声明 hosting 模块。

front_of_house.rs

1
pub mod hosting;

然后,我们再在同级下新建一个文件夹,front_of_house,对应front_of_house模块下面的模块代码入口。再在 front_of_house 文件夹下新建一个 hosting.rs。添加进我们的 add_to_waitlist 代码

front_of_house/hosting.rs

1
2
3
pub fn add_to_waitlist(){
println!("add_to_waitlist");
}

修改完后,再 cargo run 一下,编译没有问题,依旧能够通过路径正常访问到 add_to_waitlist 函数~这就是模块层级路径和文件层级路径的对应关系,只需要声明模块后,给出一个模块的对应入口文件,就可以通过这种方式拆分为多个模块文件啦。

好,我们再来试试跨文件使用代码吧。

还记得我们一开始 cargo new 是 new 了一个名为 test 的包吧?lib.rs 作为库单元包,其实就是相当于 test 包的入口文件,用来声明 test 包下的单元包。

还记得我们之前使用了 pub use 把 hosting 引入到了 lib.rs 的作用域,也就是相当于引入到了 test 包的作用域。基于这个前置知识,我们就可以在 main.rs 里跨文件使用 add_to_waitlist 函数了。

1
2
3
4
5
use test::hosting;
fn main() {
println!("Hello, world!");
hosting::add_to_waitlist();
}

运行成功输出 add_to_waitlist,也就是 pub use 在 test 包的作用域下声明了一个公有的字段 hosting,我们就可以通过路径,在 test 下访问到 hosting了。

当然,如果没有 pub use 我们也可以通过 use test::front_of_house::hosting; 的绝对路径去访问 hosting,但是注意,因为我们的 front_of_house 在 lib.rs 不是公有声明,所以 test 作为父级,是访问不到的,必须要声明为公有模块才可以访问(同理普通的 use 不能访问也是一个原理)。


第七章结束,也是常规一章的知识,总结一下这章的内容就是

  1. 模块的定义和使用方法
  2. 公有私有的权限管理,用结构体和枚举类型为例子更详细的说明了父子权限的关系
  3. use 关键字的原理和用法
  4. 模块层级路径和文件层级路径可以相互转换管理

今天就到这!

  • 标题: 【Rust 学习记录】7. 包、单元包和模块
  • 作者: TwoSix
  • 创建于 : 2023-04-01 22:41:12
  • 更新于 : 2024-07-04 23:52:28
  • 链接: https://twosix.page/2023/04/01/【Rust-学习记录】7-包、单元包和模块/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论