Skip to content

7. Package、Crate 和 Module

7.1 Package、Crate 和 Module 的定义

Rust 的代码组织包括:

  • 哪些细节可以暴露,哪些细节私有
  • 作用域内哪些名称有效

模块系统:

  • 包(Package):Cargo 的一个功能,可以让我们构建、测试、共享 crate
  • 单元包(Crate):一个模块树,可产生一个二进制文件或库
  • 模块(Module):控制代码组织、作用域和路径
  • 路径(Path):用于引用模块树中的项

Crate 的类型为 binary 和 library。src/main.rs 是二进制 crate 的入口,src/lib.rs 是库 crate 的入口。Crate Root 是编译器从 crate 开始的文件。

我们也可以在 src/bin 目录下创建多个二进制 crate。

一个 Package 包含一个 Cargo.toml 文件,描述如何构建 crate。只能包含一个库 crate,可以包含任意多个二进制 crate。但至少包含一个 crate。

Crate 可以将相关功能组合到一个作用域内,便于在项目间进行共享,而且能防止命名冲突。例如 rand crate 包含了随机数生成器,访问它的功能需要使用它的名字并 use rand

定义 Module 来控制作用域和私有性。在一个 crate 中,模块将代码进行分组。控制项目(Item)的可见性,可以使用 pub 关键字。

使用 mod 关键字创建模块。模块树形结构,模块可以包含在其他模块中。模块可以嵌套。

rust
mod front_of_house {
    mod hosting {
        fn add_to_waitlist() {}
    }
}

src/main.rssrc/lib.rs 文件是 crate root。这两个文件形成了名为 crate 的模块,位于模块树的根部。

7.2 路径

可以使用 crate 关键字来从根模块开始访问其他模块,也可以使用 super 关键字来访问父模块。也可以在模块的同一级别使用相对路径。

rust
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

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

    // 相对路径
    front_of_house::hosting::add_to_waitlist();
}

模块不仅能组织代码,还能定义私有边界(privacy boundary)。如果想把函数或 struct 设为私有,可以方到模块内部。Rust 中所有条目默认是私有的,使用 pub 关键字来使其公有。

父模块无法访问子模块中的私有内容,但子模块可以访问父模块中的内容。

7.3 父级模块路径

使用 super 关键字来访问父级模块的内容。

rust
fn serve_order() {}

mod back_of_house {
    fn fix_incorrect_order() {
        cook_order();
        super::serve_order();
    }

    fn cook_order() {}
}

使用 pub struct 来定义公有结构体,但结构体的字段默认是私有的。可以使用 pub 关键字来使字段公有。

rust
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 关键字。

7.4 使用 use 关键字

使用 use 关键字将路径引入作用域,可以使用相对路径或绝对路径。

一般对于 structenum 和函数,直接使用 use 关键字将其引入作用域,而不是引入其父级。

rust
use std::collections::HashMap;

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

同名模块必须指定到父级,或者使用 as 关键字重命名。

rust
use std::fmt::Result;
use std::io::Result as IoResult;

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

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

使用 use 引入的名称对于外部是私有的。如果想要将名称变为公有,可以使用 pub use

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

7.5 使用外部包

要使用外部包,可以在 Cargo.toml 文件中添加依赖项。Cargo 会在 crates.io 上查找依赖项。

下载好之后使用 use 关键字引入。

可以使用嵌套路径来引入外部包的子模块,语法为 use xxx::{yyy, zzz}

rust
use std::{cmp::Ordering, io};

如果要引入当前名称,使用 self 关键字。

rust
// use std::fmt::Result;
// use std::fmt;
use std::fmt::{self, Result};

我们可以使用 * 通配符来引入所有公有项。

rust
use std::collections::*;

7.6 模块的文件系统

如果定义模块时,直接使用分号而不是代码块,则 Rust 会从同名文件中加载模块。

rust
mod front_of_house;

模块层级与文件系统层级一致。例如 src/front_of_house.rssrc/front_of_house/hosting.rs

模块文件组织示例

假设我们有以下项目结构:

text
src/
├── main.rs
├── lib.rs
├── utils.rs
└── models/
    ├── mod.rs
    ├── user.rs
    └── product.rs

src/lib.rs 中声明模块:

rust
pub mod utils;
pub mod models;

src/utils.rs 中定义实用函数:

rust
pub fn format_price(price: f64) -> String {
    format!("${:.2}", price)
}

pub fn validate_email(email: &str) -> bool {
    email.contains('@')
}

src/models/mod.rs 中声明子模块:

rust
pub mod user;
pub mod product;

src/models/user.rs 中定义 User 结构体:

rust
pub struct User {
    pub id: u32,
    pub name: String,
    pub email: String,
}

impl User {
    pub fn new(id: u32, name: String, email: String) -> Self {
        User { id, name, email }
    }
}

src/models/product.rs 中定义 Product 结构体:

rust
pub struct Product {
    pub id: u32,
    pub name: String,
    pub price: f64,
}

impl Product {
    pub fn new(id: u32, name: String, price: f64) -> Self {
        Product { id, name, price }
    }
}

src/main.rs 中使用这些模块:

rust
use my_crate::models::{user::User, product::Product};
use my_crate::utils;

fn main() {
    let user = User::new(1, String::from("Alice"), String::from("alice@example.com"));
    let product = Product::new(101, String::from("Laptop"), 999.99);
    
    println!("用户: {}", user.name);
    println!("产品: {}", product.name);
    println!("价格: {}", utils::format_price(product.price));
    println!("邮箱有效: {}", utils::validate_email(&user.email));
}

7.7 工作空间(Workspace)

对于大型项目,可以使用工作空间来组织多个相关的包:

在项目根目录创建 Cargo.toml

toml
[workspace]
members = [
    "server",
    "client",
    "common",
]

这样可以在一个目录下管理多个相关的 crate,它们共享一个 Cargo.lock 文件和 target 目录。

工作空间示例

项目结构:

text
my-workspace/
├── Cargo.toml
├── Cargo.lock
├── target/
├── server/
│   ├── Cargo.toml
│   └── src/
│       └── main.rs
├── client/
│   ├── Cargo.toml
│   └── src/
│       └── main.rs
└── common/
    ├── Cargo.toml
    └── src/
        └── lib.rs

根目录的 Cargo.toml

toml
[workspace]
members = ["server", "client", "common"]

server/Cargo.toml 中可以引用同一工作空间的其他 crate:

toml
[package]
name = "server"
version = "0.1.0"
edition = "2021"

[dependencies]
common = { path = "../common" }

7.8 重新导出(Re-exporting)

使用 pub use 可以重新导出项,使其在当前模块的公共接口中可用:

rust
mod internal {
    pub mod utils {
        pub fn helper() {
            println!("Helper function");
        }
    }
}

// 重新导出,使外部可以直接使用 my_crate::helper()
pub use internal::utils::helper;

这在设计库的公共 API 时非常有用,可以简化用户的导入路径。

实际应用示例

假设我们正在开发一个图形库:

rust
// src/shapes/mod.rs
mod circle;
mod rectangle;
mod triangle;

pub use circle::Circle;
pub use rectangle::Rectangle;
pub use triangle::Triangle;

用户可以这样使用:

rust
use graphics::shapes::{Circle, Rectangle, Triangle};

// 而不是
// use graphics::shapes::circle::Circle;
// use graphics::shapes::rectangle::Rectangle;
// use graphics::shapes::triangle::Triangle;

7.9 条件编译

Rust 支持根据不同的条件编译不同的代码:

rust
#[cfg(target_os = "linux")]
fn platform_specific() {
    println!("这是 Linux");
}

#[cfg(target_os = "windows")]
fn platform_specific() {
    println!("这是 Windows");
}

#[cfg(test)]
mod tests {
    #[test]
    fn test_example() {
        assert_eq!(2 + 2, 4);
    }
}

常用的条件编译属性:

  • #[cfg(test)]:只在测试时编译
  • #[cfg(debug_assertions)]:只在调试模式编译
  • #[cfg(target_os = "...")]:特定操作系统
  • #[cfg(feature = "...")]:特定特性启用时

7.10 模块最佳实践

  1. 保持模块小而专注:每个模块应该有单一职责
  2. 使用明确的命名:模块名应该清晰表达其用途
  3. 合理使用可见性:默认设为私有,只公开必要的接口
  4. 避免循环依赖:模块之间不应该相互依赖
  5. 使用 mod.rs 或模块同名文件:根据项目规模选择合适的组织方式
  6. 文档注释:为公共模块和函数添加文档注释
rust
/// 用户管理模块
/// 
/// 提供用户的创建、查询、更新和删除功能
pub mod user {
    /// 表示系统中的用户
    pub struct User {
        pub id: u32,
        pub name: String,
    }
    
    impl User {
        /// 创建一个新用户
        /// 
        /// # Examples
        /// 
        /// ```
        /// let user = User::new(1, "Alice".to_string());
        /// assert_eq!(user.id, 1);
        /// ```
        pub fn new(id: u32, name: String) -> Self {
            User { id, name }
        }
    }
}

通过合理的模块组织,可以使项目结构清晰、易于维护和扩展。模块系统是 Rust 编写大型项目的重要工具。