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
关键字创建模块。模块树形结构,模块可以包含在其他模块中。模块可以嵌套。
mod front_of_house {
mod hosting {
fn add_to_waitlist() {}
}
}
src/main.rs
和 src/lib.rs
文件是 crate root。这两个文件形成了名为 crate
的模块,位于模块树的根部。
7.2 路径
可以使用 crate
关键字来从根模块开始访问其他模块,也可以使用 super
关键字来访问父模块。也可以在模块的同一级别使用相对路径。
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
关键字来访问父级模块的内容。
fn serve_order() {}
mod back_of_house {
fn fix_incorrect_order() {
cook_order();
super::serve_order();
}
fn cook_order() {}
}
使用 pub struct
来定义公有结构体,但结构体的字段默认是私有的。可以使用 pub
关键字来使字段公有。
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
关键字将路径引入作用域,可以使用相对路径或绝对路径。
一般对于 struct
、enum
和函数,直接使用 use
关键字将其引入作用域,而不是引入其父级。
use std::collections::HashMap;
fn main() {
let mut map = HashMap::new();
map.insert(1, 2);
}
同名模块必须指定到父级,或者使用 as
关键字重命名。
use std::fmt::Result;
use std::io::Result as IoResult;
fn function1() -> Result {
// --snip--
}
fn function2() -> IoResult<()> {
// --snip--
}
使用 use
引入的名称对于外部是私有的。如果想要将名称变为公有,可以使用 pub use
。
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}
。
use std::{cmp::Ordering, io};
如果要引入当前名称,使用 self
关键字。
// use std::fmt::Result;
// use std::fmt;
use std::fmt::{self, Result};
我们可以使用 *
通配符来引入所有公有项。
use std::collections::*;
7.6 模块的文件系统
如果定义模块时,直接使用分号而不是代码块,则 Rust 会从同名文件中加载模块。
mod front_of_house;
模块层级与文件系统层级一致。例如 src/front_of_house.rs
和 src/front_of_house/hosting.rs
。
模块文件组织示例
假设我们有以下项目结构:
src/
├── main.rs
├── lib.rs
├── utils.rs
└── models/
├── mod.rs
├── user.rs
└── product.rs
在 src/lib.rs
中声明模块:
pub mod utils;
pub mod models;
在 src/utils.rs
中定义实用函数:
pub fn format_price(price: f64) -> String {
format!("${:.2}", price)
}
pub fn validate_email(email: &str) -> bool {
email.contains('@')
}
在 src/models/mod.rs
中声明子模块:
pub mod user;
pub mod product;
在 src/models/user.rs
中定义 User 结构体:
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 结构体:
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
中使用这些模块:
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
:
[workspace]
members = [
"server",
"client",
"common",
]
这样可以在一个目录下管理多个相关的 crate,它们共享一个 Cargo.lock
文件和 target
目录。
工作空间示例
项目结构:
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
:
[workspace]
members = ["server", "client", "common"]
在 server/Cargo.toml
中可以引用同一工作空间的其他 crate:
[package]
name = "server"
version = "0.1.0"
edition = "2021"
[dependencies]
common = { path = "../common" }
7.8 重新导出(Re-exporting)
使用 pub use
可以重新导出项,使其在当前模块的公共接口中可用:
mod internal {
pub mod utils {
pub fn helper() {
println!("Helper function");
}
}
}
// 重新导出,使外部可以直接使用 my_crate::helper()
pub use internal::utils::helper;
这在设计库的公共 API 时非常有用,可以简化用户的导入路径。
实际应用示例
假设我们正在开发一个图形库:
// src/shapes/mod.rs
mod circle;
mod rectangle;
mod triangle;
pub use circle::Circle;
pub use rectangle::Rectangle;
pub use triangle::Triangle;
用户可以这样使用:
use graphics::shapes::{Circle, Rectangle, Triangle};
// 而不是
// use graphics::shapes::circle::Circle;
// use graphics::shapes::rectangle::Rectangle;
// use graphics::shapes::triangle::Triangle;
7.9 条件编译
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 模块最佳实践
- 保持模块小而专注:每个模块应该有单一职责
- 使用明确的命名:模块名应该清晰表达其用途
- 合理使用可见性:默认设为私有,只公开必要的接口
- 避免循环依赖:模块之间不应该相互依赖
- 使用
mod.rs
或模块同名文件:根据项目规模选择合适的组织方式 - 文档注释:为公共模块和函数添加文档注释
/// 用户管理模块
///
/// 提供用户的创建、查询、更新和删除功能
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 编写大型项目的重要工具。