使用模块和 Crate 组织代码

RustBeginner
立即练习

简介

随着 Rust 程序不断增长,把所有类型和函数都放在一个文件中会让职责变得难以区分。你已经在 CLI 实验中接触过库和二进制文件,也在测试实验中接触过测试模块。本实验将这些准备好的示例整合到一个模型中:软件包(package)是 Cargo 构建的项目,crate 是编译单元,模块(module)用于组织 crate 中的名称。

你将组织一个小型 pantry-report 软件包。文件和大部分实现代码都已准备好,因此每个步骤只关注一个边界:从库 crate 中共享内容、声明文件模块,以及将一个小型公共 API 导入二进制 crate。

在 Crate 之间共享函数

在本步骤中,你将识别软件包包含的两个 crate,并让二进制 crate 可以使用一个库函数。

进入准备好的 Cargo 软件包目录:

cd /home/labex/project/pantry-report

Cargo.toml 描述了一个名为 pantry-report 的软件包。Cargo 会将 src/lib.rs 识别为库 crate 的根文件,将 src/main.rs 识别为二进制 crate 的根文件。它们属于同一个软件包,但会作为独立的 crate 分别编译。

查看这两个较短的 crate 根文件。sed -n 命令只打印指定的行范围,不会修改文件:

sed -n '1,120p' src/lib.rs
sed -n '1,120p' src/main.rs

二进制 crate 通过库 crate 的路径导入 report_title。在 Rust 源代码中,包含连字符的软件包名称会转换为包含下划线的 crate 名称,因此 pantry-report 会变成 pantry_report

准备好的返回类型 &'static str 表示一个借用的字符串字面量,该字符串在整个程序运行期间都有效。'static 标记是生命周期注解。显式生命周期规则不在本入门课程的范围内;本实验不要求你理解或编写生命周期注解。

除非使用 pub 标记,否则条目默认只对其所在模块私有。打开库根文件:

nano src/lib.rs

只将函数声明从:

fn report_title() -> &'static str {

改为:

pub fn report_title() -> &'static str {

pub 会将该函数加入库 crate 的公共接口。按 Ctrl+O 保存,按 Enter 确认,然后按 Ctrl+X 退出。

使用 cargo check 检查两个 crate 的类型,但不生成最终的可运行构建文件:

cargo check

然后运行二进制 crate:

cargo run --quiet

输出应为:

Pantry Report

这说明二进制 crate 已经能够跨越 crate 边界,调用公开的库函数。

声明文件模块

在本步骤中,你将把准备好的 inventory.rs 文件连接到库 crate 的模块树。

Rust 源文件仅仅存在并不会自动参与编译。crate 根文件必须声明对应模块。对于名为 inventory 的声明,Rust 会查找 src/inventory.rs,并将其中的条目放在 inventory::... 路径下。

查看准备好的模块文件。和之前一样,sed -n '1,200p' 使用 -n 禁止自动输出,并通过 1,200p 只打印指定的行范围:

sed -n '1,200p' src/inventory.rs

#[derive(Debug)] 这一行要求 Rust 为 Item 生成标准的调试格式化支持。当前可见的程序并不依赖此功能,因此将它视为已准备好的元数据,而不是新的要求。Item 类型、它的字段以及 describe 都是公开的。辅助函数 availability 没有 pub,因此仍然只在该模块内部可用。describe 可以调用这个私有辅助函数,而模块外部的调用者只能获得公开的结果。

打开库根文件:

nano src/lib.rs

// MODULE_DECLARATION 替换为:

pub mod inventory;

开头的 pub 会通过库 crate 暴露该模块。mod inventory; 部分则将这个文件连接到模块树中。保存并退出 Nano,然后检查软件包:

cargo check
Finished `dev` profile ...

检查成功表示 Rust 已找到 src/inventory.rs,并将其作为 pantry_report::inventory 编译。

导入并使用公共 API

在本步骤中,你将使用 use 声明把模块条目引入二进制 crate,并打印一条 pantry 记录。

如果不使用导入,完整路径分别是 pantry_report::inventory::Itempantry_report::inventory::describeuse 声明会在当前作用域中创建较短的名称,不会移动或复制这些条目。

打开二进制源文件:

nano src/main.rs

// INVENTORY_IMPORT 替换为以下分组导入:

use pantry_report::inventory::{describe, Item};

大括号将共享相同路径前缀的两个条目分组。接下来,将 // INVENTORY_REPORT 替换为:

    let lentils = Item {
        name: String::from("lentils"),
        quantity: 4,
    };
    println!("{}", describe(&lentils));

由于类型和两个字段都是公开的,二进制 crate 可以构造 Item。它会将该条目的借用传递给 describe;这个公开函数会在自己的模块内部调用私有的 availability

保存并退出 Nano,然后运行软件包:

cargo run --quiet
Pantry Report
lentils: 4 jars (stocked)

这两行输出表明,crate 级别的共享和模块级别的组织都已通过一个小型公共 API 正常工作。

总结

你操作了一个包含库 crate 和二进制 crate 的 Cargo 软件包,将库条目设为公开,声明了文件模块,使用 crate 和模块路径,通过 use 缩短了这些路径,并将实现辅助函数隐藏在一个小型公共 API 后面。