1. Cargo 创建与模块化组织 Rust 项目

Cargo 创建与模块化组织 Rust 项目 学习卡片

为什么需要“二进制 + 库”混合项目

当项目既有可执行入口,又希望把核心逻辑做成可复用、可测试的库时,Cargo 的标准做法是:同一个包内同时提供 src/main.rs 与 src/lib.rs。二进制目标负责启动与参数解析,库目标承载业务模块,测试代码直接针对库模块编写,避免把逻辑塞进 main 后难以单测。

本案例创建一个名为 taskhub 的混合项目:库中按职责拆分为 models、store、service 三个模块,二进制入口只做调用与输出。通过 mod 声明模块、pub use 收敛对外接口,最后用 cargo build 与 cargo test 验证编译与测试均通过。

创建项目与目录结构

在终端执行以下命令,Cargo 会自动生成二进制与库两个目标文件:

cargo new taskhub
cd taskhub

随后手动补充模块文件,最终目录结构如下(src/lib.rs 与 src/main.rs 由 Cargo 生成,其余为新增):

taskhub/
├── Cargo.toml
└── src/
    ├── main.rs          # 二进制入口
    ├── lib.rs           # 库根,声明并导出模块
    ├── models.rs        # 数据结构
    ├── store.rs         # 存储与查询
    └── service.rs       # 业务逻辑

模块划分与可见性设计

模块拆分的核心是职责边界与可见性控制:models 只定义数据结构;store 持有数据并提供增删查;service 组合 store 完成业务动作。库根 lib.rs 用 pub use 把常用类型与函数提升到 crate 顶层,调用方无需关心内部层级。

taskhub 包内模块依赖与可见性关系 Cargo.toml [package] name = "taskhub" src/main.rs 二进制目标 (bin) use taskhub::… src/lib.rs 库根 (lib) · mod 声明 pub use 重导出 models.rs pub struct Task store.rs pub struct TaskStore service.rs pub fn add_task #[cfg(test)] mod tests 针对库模块编写单元测试 依赖 mod models mod store mod service 调用 使用 测试库 可见性规则 lib.rs 中 pub use 将 models::Task、store::TaskStore、service::add_task 提升到 crate 顶层 main.rs 通过 use taskhub::… 访问库,不直接引用内部模块路径 service 依赖 store 与 models,方向为单向 测试模块位于库内,可访问 crate 内部项

关键代码实现

src/models.rs — 定义任务数据结构,字段对外可见:

#[derive(Debug, Clone, PartialEq)]
pub struct Task {
    pub id: u32,
    pub title: String,
    pub done: bool,
}

impl Task {
    pub fn new(id: u32, title: &str) -> Self {
        Task { id, title: title.to_string(), done: false }
    }
}

src/store.rs — 内存存储,提供添加与查询:

use crate::models::Task;

#[derive(Default)]
pub struct TaskStore {
    tasks: Vec<Task>,
}

impl TaskStore {
    pub fn add(&mut self, task: Task) {
        self.tasks.push(task);
    }

    pub fn all(&self) -> &[Task] {
        &self.tasks
    }

    pub fn count(&self) -> usize {
        self.tasks.len()
    }
}

src/service.rs — 业务逻辑,组合 store 与 models:

use crate::models::Task;
use crate::store::TaskStore;

pub fn add_task(store: &mut TaskStore, id: u32, title: &str) -> Task {
    let task = Task::new(id, title);
    store.add(task.clone());
    task
}

pub fn pending_count(store: &TaskStore) -> usize {
    store.all().iter().filter(|t| !t.done).count()
}

src/lib.rs — 声明模块并用 pub use 收敛接口:

pub mod models;
pub mod store;
pub mod service;

pub use models::Task;
pub use service::{add_task, pending_count};
pub use store::TaskStore;

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn add_and_count() {
        let mut store = TaskStore::default();
        add_task(&mut store, 1, "write docs");
        add_task(&mut store, 2, "review PR");
        assert_eq!(store.count(), 2);
        assert_eq!(pending_count(&store), 2);
    }

    #[test]
    fn task_defaults_to_pending() {
        let t = Task::new(9, "deploy");
        assert!(!t.done);
        assert_eq!(t.title, "deploy");
    }
}

src/main.rs — 二进制入口,只调用库的公开接口:

use taskhub::{add_task, pending_count, TaskStore};

fn main() {
    let mut store = TaskStore::default();
    add_task(&mut store, 1, "set up cargo project");
    add_task(&mut store, 2, "split modules");

    println!("total tasks: {}", store.count());
    println!("pending: {}", pending_count(&store));
    for t in store.all() {
        println!("  [{}] {} (done={})", t.id, t.title, t.done);
    }
}

编译与测试验证

在项目根目录依次执行:

cargo build
cargo test
cargo run

预期结果:cargo build 编译通过,生成 target/debug/taskhub;cargo test 运行 2 个单元测试并全部通过(输出 test result: ok. 2 passed);cargo run 打印总任务数与待办数,并逐条列出任务。

若 cargo build 报 unresolved import,优先检查 lib.rs 中是否已用 pub mod 声明对应模块;若 main.rs 无法访问类型,检查 lib.rs 是否用 pub use 将其重导出到 crate 顶层。模块文件缺失时,Cargo 会提示 file not found for module,按提示补齐 src/ 下的文件即可。
pub use 只改变对外暴露路径,不改变模块内部可见性。若希望某个类型仅在 crate 内使用,应使用 pub(crate) 而非 pub,避免把内部实现细节暴露给下游依赖。