07使用包、Crate和模块

模块系统(the module system)

  • 包(packages): Cargo 的一个功能,它允许你构建、测试和分享 crate;
  • Crates:一个模块的树形结构,它形成了库或二进制项目;
  • 模块(modules)和 use :允许你控制作用域和路径的私有性;
  • 路径(path):一个命名例如结构体、函数或模块等项的方式;

包和crate

包(package):我们通过 cargo new <my-project> 命令创建出来的 my-project 项目中,my-project 就是我们的包名。

crate 是一个二进制项或者库。

crate root 是一个源文件,Rust 编译器以它为起点,并构建你的 crate 的根模块。

包(package)是提供一系列功能的一个或多个 crate。

  • 一个包会包含一个 Cargo.toml 文件,阐述如何取构建这些 crate。

包中包含的内容由以下几条规则来确立:

  1. 一个包中至多只能包含一个库 crate(library crate);
  2. 包中可以包含任意多个二进制 crate(binary crate);
  3. 包中至少包含一个 crate(库 crate / 二进制 crate);

当我们使用 cargo new my-project 的方式创建新项目my-project的时候,Cargo 会自动创建src/main.rs 的文件

  • my-project 就是我们当前项目的包(package);

  • Cargo 遵守了一个约定,

    • src/main.rs 就是一个与包同名的二进制 crate 的 crate 根;
    • src/lib.rs ,则是一个与包同名的 库 crate,且 src/lib.rs 是 crate 根;
1
2
3
4
5
6
// cargo 命令创建项目

// 创建的是默认包含 src/main.rs 的二进制 crate
cargo new <my-project>
// 创建的是默认包含 src/lib.rs 的库 crate
cargo new --lib <my-project>

crate 根文件的作用:

  • Cargo 会将 crate 根文件传递给 rustc 来实际构建库或者二进制项目。

一个包如何拥有多个二进制 crate ?

  • 通过将文件放在 src/bin 目录下;
  • 每个 src/bin 下的文件都会被编译成为一个独立的二进制 crate;

一个 crate 的功能是在自身作用域中进行命名的,例如,当我们将 rand 作为一个依赖,编译器不会混淆 Rng 这个名字的指向。我们可以通过rand::Rng 的方式来访问 rand crate 中的 Rng trait,而我们也可以在我们的 crate 中,创建自己的 Rng struct,指向不一样。

定义模块来控制作用域与私有性

模块 的作用:

  • 将一个 crate 中的代码分组。提高可读性与重用性。
  • 控制项的私有性,
    • 即可以被外部代码访问的 public;
    • 作为内部实现的,不能被外部代码使用的 private;
      • Rust 默认所有项(函数、方法、结构体、枚举、模块和常量)都是 private 的;
      • 父模块中的项不能使用子模块中的私有项,但是子模块中的项可以使用它们父模块中的项。
      • 可以使用 [pub ](# 使用 pub 关键字暴露路径)关键字创建公共项,使子模块的内部部分暴露给上级模块。
      • 模块(mod)上的 pub 关键字只允许其父模块引用它。样例参考 [pub](# 使用 pub 关键字暴露路径)

定义模块

1
2
3
4
5
mod <模块名> {  // 花括号内的是模块的主体;
// 模块主体;
// 可以定义其他模块;
// 可以定义例如结构体、枚举、常量、trait、函数等其他项;
}

例如:

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

mod serving {
fn take_order() {}
}
}

模块树(module tree)

  • 从 crate 根文件开始向下展示模块嵌套的树结构;

  • 如果一些模块定义在同一模块中,则它们是互为兄弟(siblings)的;

  • 如果模块A包含模块B,则A是B的父(parent),B是A的子(child);

注意:整个模块树都在名为 crate 的隐式模块下,即 crate 根文件下;

1
2
3
4
5
6
7
crate  // 这里是隐式的,Rust 中使用 crate 表示根,就像路径中的 / 根路径一样。
└── front_of_house // 定义在crate 根文件中的顶层 mod
├── hosting
│ ├── add_to_waitlist
│ └── seat_at_table
└── serving
└── take_order

这些类似与文件系统

路径用于引用模块树中的项

Rust 中使用路径的方式在模块中找到一个项的位置

  • 就像文件系统使用路径一样。

如果想要调用一个函数,我们需要知道它的路径。

路径的两种形式:

  • 绝对路径(absolute path),从 crate 根开始,以 crate 名或者字面值 crate(crate) 开头。
  • 相对路径(relative path),从当前模块开始,以 selfsuper 或当前模块的标识符开头。

路径分割使用 :: 双冒号。

如何使用绝对路径和相对路径?

  • 取决于如何管理模块;
  • 我们更倾向使用绝对路径,因为它更适合移动代码定义和项调用的相对独立;

使用 pub 关键字暴露路径

1
2
3
4
5
6
7
8
9
10
mod front_of_house {  // 因为同级,是兄弟,所以mod这里不需要添加 pub;
pub mod hosting { // 没有pub 关键字 下面hosting和add_to_waitlist会报错;
pub fn add_to_waitlist() {}
}
}

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

使用 super 起始的相对路径

可以使用 super 开头来构建从父模块开始的相对路径。

  • 这类似于文件系统中以 .. 来头的语法

使用场景:

  • 也属于相对路径,所以适用于重新组织 crate 的模块树时,一起移动提供者与调用者的场景中。
1
2
3
4
5
6
7
8
9
10
fn serve_order() {}

mod back_of_house {
fn fix_incorrect_order() {
cook_order();
super::serve_order(); // 这里的 super 就是fix_incorrect_order 的上一层 .. ,即代指 back_of_house 这一层
}

fn cook_order() {}
}

注意:superself 在模块路径中使用的作用主要有两个:

  1. 消除访问项的歧义;
  2. 防止不需要的路径硬编码;

例如:

1
2
3
4
5
6
7
8
9
// 这里self 1. 起消除歧义的作用,与下面的function区分开;
// 2. 代替模块名的硬编码,例如my_mod:function();
self::function();
function();


// 这里self 和 super,有代替模块名硬编码的作用
self::cool::function();
super::function();

创建共有的结构体和枚举

额外注意事项:

结构体:

  1. 结构体定义前添加 pub 关键字,这个结构体会变成公有的,但是结构体中的字段仍然是私有的,需要根据情况单独为字段添加 pub 关键字。
  2. 如果结构体具有私有字段,这个结构体需要提供一个公共的关联函数来创建实例,否则我们无法在外部调用时创建结构体的实例。
    • 因为我们不能在外部设置私有字段的值;
    • 结构体通常使用时,不必将它们的字段公有化;

枚举:

  1. 如果枚举设为公有,则它的所有成员都将变成公有的。我们只需要在 enum 关键字前添加 pub 关键字。
    • 如果枚举成员不是公有的,那么枚举会显得用处不大;
1
2
3
4
5
6
mod back_of_house {
pub enum Appetizer {
Soup,
Salad,
}
}

使用 use 关键字将名称引入作用域

使用 use 关键字,可以一次性将路径引入作用域,方便我们调用该路径中的项。

  • 与 Java 和 python 中的 import 类似;
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;

pub fn eat_at_restaurant() {
hosting::add_to_waitlist(); // 这里调用函数时指定父模块,可以清晰地表明函数不是在本地定义的;
hosting::add_to_waitlist();
hosting::add_to_waitlist();
}

注意:use 引入时,use + path 的方式,我们通常引入到调用项所在模块即可,使用时 模块名+调用项

  1. 这样在调用函数时,通过指定父模块,可以清晰地表明函数不是在本地定义的
  2. 同时使完整路径的重复度最小化;
  3. 使用 use 引入结构体、枚举和其他项时,习惯是指定它们的完整路径。
  4. 如果两个模块具有相同的名字,但是父模块名字不同时,我们通常引入到它们的父模块;(Rust 不允许直接引入具有相同名字模块的完整路径,因为直接使用该模块时,不容易分清是哪个模块的。)

第1,2,3点的样例

1
2
3
4
5
6
7
8
// 将 HashMap 结构体引入作用域的习惯用法;
// 指定到该结构体的完整路径即可。
use std::collections::HashMap;

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

第4点的样例:

1
2
3
4
5
6
7
8
9
10
11
12
use std::fmt;
use std::io;
// 这样的引入路径,我们就可以比较容易分清两个Result分别是哪个模块中的了。
fn function1() -> fmt::Result {
// --snip--
# Ok(())
}

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

使用 as 关键字提供新的名称

针对使用 use 将两个同名类型引入同一作用域这个问题,还有另一个解决办法:

  • 使用 as 关键字,即 在这个类型的路径后面,使用 as 指定一个新的本地名称或者别名。
    • 这点与 python 很像;

例如:

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(())
}

使用 pub use 重导出名称

使用 use 将路径(名称)导入到作用域内后,该名称在此作用域内是私有的。

pub use作用:(这个组合更多用在将 crate 对外发布使用的场景。)

  • 将条目引入作用域;
    • (在当前作用域与使用 use 效果一样)
  • 该条目可以被外部代码引入到它们的作用域;
    • (通常我们自己的模块代码要作为 库 crate 对外发布的情况下,最好使用 pub use 的形式,这样外部调用者就可以根据我们 pub use 的路径引入模块和项了,而不用关心我们真正实现时的路径)

样例:

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 use crate::front_of_house::hosting;

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

还可以参考 在文档 中的样例效果。

使用外部包

当我们的项目需要引入外部包时,

  1. 首先在项目的 Cargo.toml 文件的 dependencies段落中加入依赖的包名;
    • 要从crates.io 中下载,可以修改源地址加快下载速度;
  2. 使用 use 将模块引入项目作用域;

注意:标准库(std)对于我们的包来说也是外部 crate。因为标准库随 Rust 语言一同发布,无需修改 Cargo.toml 来引入 std,不过依然需要使用 use 将标准库中定义的项引入项目包的作用域中。例如 HashMap。

嵌套路径来消除大量的 use

语法

1
指定路径的相同部分::{[self,]子路径各自不同的部分1,不同2...}

样例:

1
2
3
4
5
6
7
8
9
10
11
use std::cmp::Ordering;
use std::io;
=>
use std::{cmp::Ordering, io};


// 如果存在夫模块路径和子模块路径时,父模块使用 self 关键字
use std::io;
use std::io::Write;
=>
use std::io::{self, Write};

我们可以在路径的任何层级使用嵌套路径。

通过 glob 和 * 运算符将所有的公有定义引入作用域

如果希望将一个路径下 所有 公有项引入作用域,可以指定路径后跟 * ,glob 运算符。

1
2
// 这里指将 collections 中定义的所有公有项引入当前作用域。
use std::collections::*;

注意:使用 glob 运算符时需要小心,glob 会使得我们难以推导作用域中有什么名字和它们是在何处定义的。

glob 运算符经常用于测试模块 tests 中,将所有内容引入作用域。

将模块分割进不同文件

对于 二进制 crate 或 库 crate 来说,我们可以将该文件中的多个 mod 拆分到不同的子文件或者子目录中。

模块定义时,如果模块名后边是 “;” ,而不是代码块:

  • Rust 会从与模块同名的文件中加载内容;
  • 模块树的结构不会变化;

随着模块逐渐变大,该技术可以把模块的内容移动到其他文件中。

如果拆分到子文件,

  1. 新建一个以 需要移除的 mod 名 命名的 rs 文件;

  2. 将该 mod 中的内容复制到 新 rs 文件中;

  3. 在原文件中添加 mod 声明语句(带;号),这将告诉 Rust 在另一个与模块同名的文件中加载模块的内容;

例如:

1
2
3
4
5
6
7
mod front_of_house;
pub use crate::front_of_house::hosting;
// 也可以使用相对路径
// pub use front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}

方法二,如果该 mod 还包含子 mod,可以创建 目录+子文件 的多层结构;

  1. 创建一个以 需要移动的父 mod 模块名 命名的 rs 文件;在文件中添加子 mod 的声明;
  2. 在步骤1中的同级目录下,创建一个以 需要移动的父 mod 模块名 命名的目录,和一个子 mod 名命名的 rs 文件;
  3. 移动子 mod 的内容到同名文件中;
  4. 原始文件中依然需要声明父 mod ;
  5. 在原始文件中,使用子 mod 的方式与方式一中一样。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
src
|-- front_of_house
| `-- hosting.rs
|-- front_of_house.rs
`-- main.rs

文件名: src/front_of_house.rs
pub mod hosting;

目录名:src/front_of_house
文件名: src/front_of_house/hosting.rs
pub fn add_to_waitlist() {}

此时原文件中的使用 add_to_waitlist() 的方式不变;

在拆分模块到文件的过程中,

1
2
3
4
5
6
7
8
9
10
11
12
13
|-- my
| |-- inaccessible.rs
| |-- mod.rs
| `-- nested.rs
`-- split.rs

// 上下两种方式效果相同

|-- my
| |-- inaccessible.rs
| `-- nested.rs
|-- my.rs
`-- split.rs

my.rs == my/mod.rs ,即效果是相同的,在其他模块中使用时,都是作如下声明 :

1
2
// pub 是可选的,根据访问需求添加;
[pub] mod my;

如上所示,Rust 会将该文件内容放进名为 my 的模块中,然后插入到当前声明的作用域中。

多个二进制文件如何处理

常规单个二进制文件的结构

1
2
3
4
foo
├── Cargo.toml
└── src
└── main.rs

main.rs 是默认的二进制 crate

多个二进制文件的结构

1
2
3
4
5
6
foo
├── Cargo.toml
└── src
├── main.rs
└── bin
└── my_other_bin.rs

如果希望有除默认 main.rs 之外的二进制 crate,就在 main.rs 同级目录下创建 bin 目录,然后添加二进制 crate。

二进制文件中如何引入上级模块

通过 mod 上添加路径属性,指定 mod 所在 rs 文件路径。

1
2
3
4
#[path ="../dbaccess/mod.rs"]
mod db_access;
#[path ="../errors.rs"]
mod errors;

可能会遇到的问题

如果直接 use 导入,而没有依赖进来,那么会有类似如下报错:

1
2
unresolved import `animal_test`
use of undeclared crate or module `animal_test`

解决方法:

  1. 上述错误,是本地mod,则在使用的rs文件中 使用 pub mod animal_test;
  2. 如果不是外部crate,那么需要在 Cargo.toml 文件中依赖此 crate;
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2022-2023 ligongzhao
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信