Rust 的代码组织依赖于三个核心概念:软件包(Package)代码包(Crate)模块(Module)。理解它们之间的关系以及 Cargo 的约定,对于构建可维护、可扩展的 Rust 项目至关重要。本文将详细阐述这些概念,并通过一个具体的目录结构示例,展示 Rust 项目是如何组织的。


1. 基本概念

1.1 软件包(Package)

一个软件包(Package)通常就是一个完整的软件项目,包含一个 Cargo.toml 文件,用于定义项目元数据、依赖关系以及构建配置。一个 Package 可以包含多个 Crate,但最多只能包含一个 库 Crate,而可以包含任意多个 二进制 Crate

  • 英文术语:Package
  • 表现形式:包含 Cargo.toml 的目录,例如 backyard/

1.2 代码包(Crate)

代码包(Crate)是 Rust 编译器的最小处理单元。一个 Crate 可以编译为一个二进制可执行文件(Binary)或一个库文件(Library)。Crate 将相关的功能组合在一起,并可以通过可见性控制对外暴露的 API。

  • 英文术语:Crate
  • 类型
    • 二进制 Crate(Binary Crate):包含 main 函数,编译后可生成可执行文件。
    • 库 Crate(Library Crate):不包含 main 函数,用于提供可复用的功能,其他项目可以通过依赖来引用它。

1.3 模块(Module)

模块(Module)是 Rust 中组织代码的最小单位。它允许你将相关的函数、结构体、枚举、常量等组合在一起,并控制它们的可见性(通过 pub 关键字)。模块可以嵌套,形成模块树,根模块通常是 Crate 的入口文件(如 lib.rsmain.rs)。

  • 英文术语:Module
  • 作用:代码封装、命名空间隔离、权限控制。

2. Cargo 的文件系统约定

Cargo 根据一定的约定来解析 Crate 和模块,无需手动配置。以下是关键约定:

2.1 Crate 入口文件

  • 库 Crate 入口:默认是 src/lib.rs。该文件作为一个 Crate 的根模块,其内容构成了库的公共接口。
  • 二进制 Crate 入口
    • 默认入口src/main.rs,对应一个与 Package 同名的二进制 Crate。
    • 其他二进制入口:放在 src/bin/ 目录下,每个文件(如 binary-2.rs)会被视为一个独立的二进制 Crate,编译后生成与文件名同名的可执行文件。

可以通过修改 Cargo.toml 中的 [[bin]][lib] 表来指定其他路径,但通常遵循默认约定即可。

2.2 模块声明与文件系统映射

在 Rust 中,模块通过 mod 关键字声明。Cargo 会根据模块声明的路径自动寻找对应的文件,有三种情况:

  1. 内联模块:直接在声明模块的文件中,使用 mod module_name { ... } 包含模块内容。
  2. 单文件模块:创建与模块同名的 .rs 文件,例如 module_name.rs,放在声明模块的文件的同级目录下。
  3. 带 mod.rs 的目录模块:创建一个与模块同名的目录,并在该目录下放置 mod.rs 文件作为该模块的根文件。这种形式适合包含子模块的复杂模块。

例如,如果在 lib.rs 中声明 mod garden;,Cargo 会依次尝试:

  • 查找 garden.rs 文件
  • 查找 garden/mod.rs 文件

一旦找到,该文件就成为 garden 模块的内容。在 garden 模块中又可以继续声明子模块,同样遵循上述规则。


3. 实例解析:backyard 项目

假设我们有一个名为 backyard 的 Package,其目录结构如下:

backyard/
├── Cargo.lock
├── Cargo.toml
├── target/
│   ├── debug/
│   │   ├── backyard              # main.rs 构建的可执行文件
│   │   ├── binary-2              # bin/binary-2.rs 构建的可执行文件
│   │   ├── binary-3              # bin/binary-3.rs 构建的可执行文件
│   │   └── libbackyard.rlib      # lib.rs 构建的库文件
│   └── release/
└── src/
    ├── lib.rs                    # 库 Crate 入口
    ├── main.rs                   # 默认二进制 Crate 入口
    ├── bin/
    │   ├── binary-2.rs
    │   └── binary-3.rs
    ├── garden/
    │   ├── mod.rs                # garden 模块根
    │   └── vegetables/
    │       ├── mod.rs            # vegetables 模块根
    │       ├── cabbage/
    │       │   └── mod.rs        # cabbage 子模块
    │       └── carrot/
    │           └── mod.rs        # carrot 子模块
    ├── flowers/
    │   ├── mod.rs                # flowers 模块根
    │   ├── rose/
    │   │   └── mod.rs            # rose 子模块
    │   └── clove/
    │       └── mod.rs            # clove 子模块
    └── fish-bond/
        └── mod.rs                # fish-bond 模块根(注意模块名含有连字符,需特殊处理)

3.1 项目根目录

  • Cargo.toml:项目配置文件,定义依赖、元数据等。
  • Cargo.lock:自动生成,用于锁定依赖版本,确保构建可重复。可删除,下次编译会重新生成。
  • target/:存放构建产物,debugrelease 分别对应不同模式。

3.2 Crate 入口文件

  • src/lib.rs:库 Crate 入口。它声明了项目中可被外部引用的公共模块和函数。编译后生成 libbackyard.rlib
  • src/main.rs:默认二进制 Crate 入口。它可以引用 lib.rs 中定义的公共项,从而复用库代码。
  • src/bin/:包含其他二进制 Crate 入口文件,每个文件独立编译为可执行文件。

3.3 模块树

模块的声明与文件系统紧密对应。假设在 lib.rs 中有以下声明:

// src/lib.rs
mod garden;      // 声明 garden 模块,内容在 garden/mod.rs 中
mod flowers;     // 声明 flowers 模块,内容在 flowers/mod.rs 中
mod fish_bond;   // 声明 fish_bond 模块,注意模块名必须是合法标识符
  • garden 模块的根文件是 src/garden/mod.rs。它可能包含代码,并进一步声明子模块,例如:
// src/garden/mod.rs
pub mod vegetables;  // 声明 vegetables 子模块,内容在 vegetables/mod.rs 中
  • vegetables 模块的根文件是 src/garden/vegetables/mod.rs,它又可以声明子模块:
// src/garden/vegetables/mod.rs
pub mod cabbage;  // 声明 cabbage 子模块,内容在 cabbage/mod.rs 中
pub mod carrot;   // 声明 carrot 子模块,内容在 carrot/mod.rs 中
  • 类似地,flowers 模块下也有子模块 roseclove,各自对应一个 mod.rs 文件。

注意fish-bond 目录名包含连字符,这在 Rust 模块名中是不允许的。若要使用这样的目录,需要在声明模块时使用 #[path = "fish-bond/mod.rs"] 属性指定路径,或保持模块名与目录名一致(但连字符非法)。通常建议使用下划线命名目录,如 fish_bond,并在模块声明中保持一致。这里仅为示例说明目录名可以与模块名不同,但需要特殊处理。

3.4 模块内部代码

每个 mod.rs 文件可以包含该模块的具体实现,例如函数、结构体等。如果需要将代码拆分到多个文件,也可以使用单文件模块的形式,例如将 cabbage 的代码直接放在 cabbage.rs 中(放在 vegetables 目录下),而不是用 cabbage/mod.rs


4. 构建与运行

4.1 构建命令

  • 默认(debug)构建cargo build,产物存放在 target/debug/
  • release 构建cargo build --release,启用优化,产物在 target/release/

构建产物包括:

  • 库文件:以项目名命名的 .rlib 文件(例如 libbackyard.rlib)。
  • 可执行文件:对于 main.rs,生成与项目同名的可执行文件(如 backyard);对于 bin/ 下的文件,生成与文件名同名的可执行文件(如 binary-2binary-3)。

4.2 运行命令

  • cargo run:默认运行由 main.rs 构建的可执行文件。
  • cargo run --bin binary-2:运行 bin/binary-2.rs 构建的可执行文件。

5. 补充:模块路径与可见性

为了让模块之间的内容互相访问,Rust 提供了**路径(Path)可见性(Visibility)**机制。

5.1 路径

  • 绝对路径:从 crate 根开始,以 crate 关键字开头,例如 crate::garden::vegetables::cabbage::SomeType
  • 相对路径:从当前模块开始,使用 selfsuper 或直接使用标识符,例如 super::vegetables

5.2 可见性

默认情况下,模块中的项(函数、结构体等)是私有的,只有父模块可以访问。使用 pub 关键字可以将其公开:

  • pub:对父模块及后续所有模块公开。
  • pub(crate):在整个 crate 内公开。
  • pub(super):仅在父模块内公开。
  • pub(in path):在指定路径内公开。

5.3 使用 use 引入路径

为了方便,可以使用 use 关键字将路径引入作用域,例如:

use crate::garden::vegetables::cabbage::Cabbage;

fn main() {
    let c = Cabbage::new();
}

6. 总结

Rust 通过 Package、Crate 和 Module 构建了清晰且灵活的代码组织体系。Cargo 的约定(如 lib.rsmain.rsbin/ 目录以及模块文件的查找规则)使得开发者无需繁琐的配置即可管理大型项目。掌握这些概念和约定,是编写高质量 Rust 代码的基础。

在实际开发中,合理规划模块树,利用可见性控制 API 暴露范围,结合 use 简化路径,可以有效提升代码的可读性和可维护性。希望本文的讲解能帮助你更好地理解 Rust 的项目结构,并在自己的项目中游刃有余地组织代码。