跳到主要内容

模块与 Crate

问题

Rust 的模块系统是如何组织代码的?Crate 和 Module 有什么区别?

答案

Rust 的模块系统由四个层级组成:Workspace → Package → Crate → Module

核心概念

概念说明
Crate编译的最小单元,分为 binary crate(有 main)和 library crate
Package包含 Cargo.toml 的项目,至多 1 个 library crate,任意多个 binary crate
Modulemod 关键字创建的命名空间,控制作用域和可见性
Workspace多个 Package 共享依赖和构建目录

模块定义方式

// 方式一:行内模块
mod math {
pub fn add(a: i32, b: i32) -> i32 { a + b }
fn internal() {} // 私有
}

// 方式二:独立文件 — 旧风格
// src/math.rs 或 src/math/mod.rs

// 方式三:独立文件 — 新风格(推荐)
// src/math.rs → mod math 的内容
// src/math/algo.rs → mod math::algo 的内容

文件结构映射

src/
├── main.rs ← binary crate 根
├── lib.rs ← library crate 根
├── config.rs ← mod config
├── routes/
│ ├── mod.rs ← mod routes(旧风格)
│ ├── auth.rs ← mod routes::auth
│ └── user.rs ← mod routes::user
└── services/
├── mod.rs ← mod services
└── db.rs ← mod services::db
新风格 vs 旧风格

Rust 2018+ 推荐新风格src/routes.rs + src/routes/ 子目录,而不是 src/routes/mod.rs。两者功能一致,新风格不需要大量 mod.rs 文件,方便在编辑器中区分。

可见性(Visibility)

Rust 默认一切私有,需要显式声明公开:

mod outer {
pub mod inner {
pub fn public_fn() {} // 完全公开
fn private_fn() {} // 仅 inner 内部可见

pub(crate) fn crate_fn() {} // 当前 crate 内可见
pub(super) fn parent_fn() {} // 父模块可见
pub(in crate::outer) fn scoped_fn() {} // 指定路径可见
}

fn access() {
inner::public_fn(); // ✅
// inner::private_fn(); // ❌ 私有
inner::crate_fn(); // ✅ 同 crate
inner::parent_fn(); // ✅ 是父模块
}
}

可见性修饰符总结:

修饰符可见范围
无(默认)当前模块及其子模块
pub完全公开
pub(crate)当前 crate 内
pub(super)父模块
pub(in path)指定祖先模块
结构体字段可见性

pub struct 只是让结构体名公开,字段默认仍是私有的,需要逐个标记 pub

pub struct User {
pub name: String, // 公开
age: u32, // 私有 — 外部无法直接构造
}

// 外部无法 User { name: ..., age: ... },需要提供构造函数
impl User {
pub fn new(name: String, age: u32) -> Self {
User { name, age }
}
}

use 导入

// 绝对路径(从 crate 根开始)
use crate::routes::auth::login;

// 相对路径
use self::utils::helper; // 当前模块的子模块
use super::config::Config; // 父模块

// 重命名
use std::collections::HashMap as Map;

// 嵌套导入
use std::io::{self, Read, Write}; // io 模块本身 + Read + Write

// 通配符(不推荐用于非 prelude 场景)
use std::collections::*;

// 重新导出(pub use)
pub use crate::db::connection::Pool;
// 外部可以直接 use your_crate::Pool
pub use 的作用

pub use 是 Rust 的重新导出机制,用于简化公开 API:

// 内部结构可能很深
// src/models/user/entity.rs → pub struct User

// 在 lib.rs 中重新导出
pub use models::user::entity::User;

// 外部直接用:use your_crate::User;

标准库大量使用这种模式,例如 std::io::Read 实际定义在更深的模块中。

Crate 类型

# Cargo.toml
[package]
name = "my-project"
version = "0.1.0"

# Library crate(src/lib.rs)
[lib]
name = "my_lib"

# Binary crate(src/main.rs)
[[bin]]
name = "my-app"
path = "src/main.rs"

# 多个 binary crate
[[bin]]
name = "cli-tool"
path = "src/bin/cli.rs"

一个 Package 可以同时包含 src/lib.rs(library)和 src/main.rs(binary),binary crate 可以 use library crate。

Cargo Workspace

多个 Package 共享依赖:

# 根目录 Cargo.toml
[workspace]
members = ["api", "core", "cli"]

# 统一依赖版本(Rust 1.64+)
[workspace.dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
# api/Cargo.toml
[package]
name = "api"

[dependencies]
core = { path = "../core" }
serde.workspace = true # 使用 workspace 统一版本
tokio.workspace = true

Workspace 优点:

  • 共享 target/ 目录,加速编译
  • 统一依赖版本,避免版本冲突
  • cargo build --workspace 一次构建所有

外部依赖

[dependencies]
serde = "1.0" # crates.io
serde_json = { version = "1.0", features = ["raw_value"] }
my_lib = { path = "../my_lib" } # 本地路径
my_git = { git = "https://github.com/user/repo", branch = "main" }

[dev-dependencies] # 仅测试和示例使用
mockall = "0.12"

[build-dependencies] # 构建脚本使用
cc = "1.0"

常见面试问题

Q1: Crate 和 Module 的区别?

答案

  • Crate 是编译的最小单元,编译器一次只处理一个 Crate。有两种类型:library crate(产出 .rlib)和 binary crate(产出可执行文件)
  • Module 是 Crate 内部的命名空间划分,用于组织代码和控制可见性。一个 Crate 可以包含多个 Module

类比:Crate 相当于 npm 的 package,Module 相当于文件/目录的组织结构。

Q2: use crate::xxxuse super::xxx 的区别?

答案

// crate:: —— 绝对路径,从 crate 根开始
use crate::models::User;

// super:: —— 相对路径,从父模块开始
use super::Config;

// self:: —— 从当前模块开始
use self::helper::format;

推荐在大型项目中使用 crate:: 绝对路径,因为代码重构移动模块时更稳定。

Q3: pub(crate) 什么时候用?

答案

pub(crate) 表示"对 crate 内部公开,但不暴露给外部用户"。适用于:

  • Library crate 的内部辅助函数,被多个模块共用但不属于公开 API
  • 避免用 pub 后不小心成为公开 API(语义破坏性变更)
// 外部用户看不到,但 crate 内任何模块都能用
pub(crate) fn internal_helper() { }

Q4: 为什么 Rust 不支持循环依赖?

答案

Crate 之间不能循环依赖,Module 之间可以互相引用。这是因为 Crate 是独立编译单元,循环依赖会导致编译器无法确定编译顺序。

解决方案:将被共同依赖的代码提取到独立的 Crate 中:

❌ A depends on B, B depends on A
✅ A depends on C, B depends on C

Q5: Cargo.toml 中 features 是什么?

答案

Features 是条件编译的高层抽象,允许用户选择性启用功能:

[features]
default = ["json"] # 默认启用
json = ["dep:serde_json"] # 启用 serde_json 依赖
async = ["dep:tokio"]
full = ["json", "async"] # 组合 feature

[dependencies]
serde_json = { version = "1.0", optional = true }
tokio = { version = "1", optional = true }
#[cfg(feature = "json")]
pub fn parse_json(s: &str) -> Value {
serde_json::from_str(s).unwrap()
}

用户选择需要的功能,减少编译时间和产物体积。

相关链接