【AI开发之Rust】第 10 课:Cargo 工程化 —— 模块、依赖与测试(基础篇收尾)

10.1 这节课解决什么问题

前 9 课写的都是"一个 main.rs 里全塞下"的玩具。真实工程(比如实战篇的共享核心)动辄几十个文件:modelsnetworkstoreapi......它们如何组织、如何互相引用?依赖版本怎么锁定?代码怎么保证不倒退?------工程化三件事:

rust 复制代码
模块系统   拆文件、声明可见性,组织成清晰的代码树
依赖管理   引入第三方 crate、锁版本、构建可复现
测试体系   单元 + 集成 + 文档三种测试,让行为可回归

本课也是基础篇的验收课:学完你会把 1-9 课的能力收进一个规范工程,达到"能写、能测、能提交"的程度。

10.2 模块系统:mod / use / pub

10.2.1 最小模块:文件内拆分 + 可见性

rust 复制代码
mod math {                         // 声明模块
    pub fn add(a: i32, b: i32) -> i32 { a + b }   // pub:模块外部可见
    fn secret() {}                 // 默认私有:只在本模块(及子模块)内可见
}

fn main() {
    println!("{}", math::add(1, 2));   // 模块名::路径 访问
    // math::secret();              // ❌ 私有,访问不到
}

可见性速记(默认一切私有,逐级放开):

rust 复制代码
(不写)   private:仅当前模块及其子模块可见
pub       对所有人可见(对外部 crate 也可见)
pub(crate) 仅本 crate 可见(跨模块随便用,但不对外暴露)
pub(super) 仅父模块可见

💡 从第 6 课起你写的每个 pub fnpub struct 都是这套规则的实例。规则本质只有一个:"谁需要看见谁,就显式声明",编译器强迫你把"API 边界"想清楚。

10.2.2 拆文件的规范姿势:目录即模块

模块树和文件树一一对应,这是 Rust 工程的基本组织方式。约定如下(后续每课示例都以这种结构展开):

bash 复制代码
my_app/
├── Cargo.toml
└── src/
    ├── main.rs            # crate 根:声明顶层模块
    ├── models.rs          # 顶层模块 models(对应文件 models.rs)
    └── services/          # 顶层模块 services(对应目录)
        ├── mod.rs         # services 模块入口:再声明子模块
        ├── auth.rs        # services::auth
        └── chat.rs        # services::chat
rust 复制代码
// src/main.rs ------ 用 mod 声明各顶层模块(文件名去 .rs 后缀)
mod models;                // 找到 models.rs
mod services;              // 找到 services/mod.rs

use models::User;                       // 之后直接写 User
use services::auth::login;              // 引入深路径函数

fn main() {
    let u = User::new(String::from("小灵"));
    let ok = login(&u);
    println!("登录:{ok}");
}
rust 复制代码
// src/models.rs
pub struct User {          // pub:要被 services 用
    pub name: String,      // 字段也要 pub 才能跨模块读写
}

impl User {
    pub fn new(name: String) -> User {
        User { name }
    }
}
rust 复制代码
// src/services/mod.rs ------ 目录入口:声明并"转发"子模块
pub mod auth;
pub mod chat;
rust 复制代码
// src/services/auth.rs
use crate::models::User;   // crate:: 表示"从 crate 根"找路径

pub fn login(u: &User) -> bool {
    !u.name.is_empty()
}

三种路径写法(务必分清什么时候用哪个):

rust 复制代码
crate::models::User    绝对路径:从当前 crate 根开始(跨模块引用最稳)
super::models::User    super = 父模块,相对父级找(子模块里常用)
use models::User       先用 use 引入,之后直接写 User(日常主力)

习惯约定:
  use 引入时不要写整棵长路径,use 到"够用的最近一层"即可,
  例如 use services::auth::{login, logout};

💡 Rust 中"声明"和"引用"是两件事:mod services; 是把目录挂进模块树use services::auth::login; 是把某个项引入当前作用域 。搞混时看报错:unresolved import 通常是 use 路径写错或 mod 忘声明。

10.2.3 use 的常用形态

rust 复制代码
use models::{User, Profile};                // 同一父级多个项:花括号
use services::auth::*;                      // 通配(谨慎用,会引入未知名字)
use crate::models::User as AppUser;         // as 起别名(解决同名冲突)
// use 还可以嵌在函数里,只在局部生效(罕见,但合法)

10.2.4 模块和 privacy:pub struct 但字段私有

一个常见设计模式------"构造方式受限"的领域对象

rust 复制代码
// models.rs
pub struct Session {               // 结构体对大家可见
    id: String,                    // 字段对外不可见
    created_at: u64,
}

impl Session {
    pub fn new(id: String) -> Session {   // 只能通过构造器创建
        Session { id, created_at: now_ms() }
    }

    pub fn id(&self) -> &str { &self.id }  // 只读 getter
}

fn now_ms() -> u64 { 1_700_000_000_000 }

外部无法直接拼 Session { id: ... },只能走 Session::new(...)------模块边界 = 封装边界。实战篇定义领域模型时会大量用到这种"私有字段 + 构造器 + getter",保证非法状态在编译期/构造期就被拦下。

10.3 Cargo 依赖管理

10.3.1 声明依赖与版本语义

第 7 课已经用过 thiserror/anyhow。完整形态:

toml 复制代码
[package]
name = "my_app"
version = "0.1.0"
edition = "2021"                 # 见 1.3 的 edition 说明

[dependencies]
serde = { version = "1", features = ["derive"] }   # 默认 features
tokio = { version = "1", features = ["full"] }     # 第 13 课
reqwest = { version = "0.12", default-features = false, features = ["json"] }
thiserror = "2"
anyhow = "1"

[dev-dependencies]               # 仅测试/示例需要的依赖
tempfile = "3"

[profile.release]                # 发布构建调优(16 课 FFI 发布前必调)
opt-level = 3
lto = true
codegen-units = 1

版本号 "1.6" 的语义(Cargo 遵循 semver):

ini 复制代码
"1.6"   = 允许 >=1.6 且 <2.0.0 (只会升级小版本/补丁,不会破大版本)
"=1.6.2" = 精确锁定
"1.6.2" = 允许 1.6.2..2.0.0(等价 ^1.6.2)

只要依赖方遵守 semver,写 "1" / "0.12" 这种"大版本号"通常安全;不放心就用 cargo update -p <crate> 精确升级并跑测试。

10.3.2 Cargo.lock:可复现构建

csharp 复制代码
Cargo.toml       你写"宽泛约束"(如 "1")
Cargo.lock       第一次构建后自动生成,把每个依赖钉到精确版本
                 ------ 可执行程序(bin)要把 lock 提交进 git,保证大家/CI/发布版一致
                 ------ 库项目(lib)通常忽略 lock,让使用方各自解析
  • 首次 cargo build 会解析依赖生成 lock,之后每次构建都按 lock 来;
  • 想升级某依赖:cargo update -p serde(只升 serde)或 cargo update(全部按 Cargo.toml 约束升);
  • 国内网络慢?在 $CARGO_HOME/config.toml 配镜像(第 1 课已给样例):把 [source.crates-io]replace-with 指向 rsproxy/中科大镜像即可。

10.3.3 bin / lib:包既可以是程序也可以是库

一个 crate 可以同时有可执行入口和库接口:

toml 复制代码
# 只需在 src/ 加 lib.rs,Cargo 自动识别
[lib]
name = "my_app"        # 库名(默认取 package.name)
path = "src/lib.rs"
rust 复制代码
src/
├── lib.rs        # 库根:外部 crate 通过 use my_app::... 使用
├── main.rs       # 二进制根:use my_app::... 复用库
└── ...
rust 复制代码
// lib.rs:把要"对外"的都 pub 出去
pub mod models;
pub mod services;
rust 复制代码
// main.rs:可执行文件通过包名引入库
use my_app::models::User;

fn main() {
    let u = User::new(String::from("cli"));
    println!("{:?}", u.name);   // (User 记得 derive Debug 或只用 getter)
}

💡 这是本课程项目的标准形态:逻辑全放 lib(可测试、可被上层复用),main 只做入口。实战篇 17 课会把"共享核心"建成 lib crate。

10.3.4 workspace:多个 crate 一个仓库(预告)

第 17 课实战会用 workspace 管理"核心 lib + 不同壳":

toml 复制代码
# 根 Cargo.toml
[workspace]
members = ["core", "app-cli", "ffi-bindings"]
resolver = "2"
  • 一个仓库内多个 crate 共享一个 Cargo.lock、一套依赖缓存;
  • 本地路径依赖:core 里写 [dependencies] my-core = { path = "../core" } 可直接互相引用,发布时改成 crates.io 版本即可。细节在实战篇展开,此处先建立概念。

10.4 测试体系:三种测试让代码可回归

10.4.1 单元测试(写在模块里,#[cfg(test)]

惯例:每个模块底部放 #[cfg(test)] mod tests { ... }#[cfg(test)] 表示"只在跑测试时编译这段"。

rust 复制代码
// models.rs
pub fn add(a: i32, b: i32) -> i32 { a + b }

#[cfg(test)]
mod tests {
    use super::add;                    // super:: 引用父模块的私有函数------测试能测私有!

    #[test]
    fn test_add() {
        assert_eq!(add(2, 3), 5);
        assert_ne!(add(2, 2), 5);
        assert!(add(-1, 1) == 0);
    }

    #[test]
    #[should_panic]                    // 期望 panic(如越界类防御逻辑)
    fn test_index_out_of_bounds() {
        let v = [1, 2, 3];
        let _ = v[10];
    }

    #[test]
    fn test_result_return() -> Result<(), String> {   // 测试也能返回 Result
        if add(1, 1) == 2 { Ok(()) } else { Err("加错了".into()) }
    }
}

常用断言宏:

css 复制代码
assert!(条件)                表达式为假 → 测试失败
assert_eq!(a, b)             两边相等(要 a、b 实现 PartialEq + Debug 才能打印差异)
assert_ne!(a, b)
assert_eq!(a, b, "附加信息 {}", x)   后面可跟格式串,失败时打印上下文

10.4.2 运行与过滤

bash 复制代码
cargo test              # 跑全部
cargo test test_add     # 只跑名字含 test_add 的
cargo test -- --nocapture   # 显示 println!(测试默认吞输出)
cargo test -- --test-threads=1   # 串行(有共享状态时)

10.4.3 集成测试(tests/ 目录)

tests/ 目录下的每个文件是独立 crate,只能访问 lib 的公开 API(所以代码要放 lib.rs 才测得到):

bash 复制代码
src/lib.rs
tests/
└── api_test.rs
rust 复制代码
// tests/api_test.rs
use my_app::add;           // 像外部用户一样用这个库

#[test]
fn it_adds_from_outside() {
    assert_eq!(add(40, 2), 42);
}

💡 如果项目只有 main.rs(纯二进制),tests/ 里 use 包名 会失败。想被集成测试就用 lib + 薄 main 的形态,这是把逻辑迁进 lib.rs 的又一个理由。

10.4.4 文档测试(doc tests)

代码注释里的 ```rust 代码块会被当测试跑------保证文档示例永不过期:

rust 复制代码
/// 返回两个数相加。
///
/// ```
/// let s = my_app::add(1, 2);
/// assert_eq!(s, 3);
/// ```
pub fn add(a: i32, b: i32) -> i32 { a + b }
bash 复制代码
cargo test --doc    # 单独跑文档测试

💡 第 16 课 FFI 章节会给每个 #[uniffi::export] 的接口写文档测试样例,这一条在"文档即契约"里非常实用。

10.5 代码质量三件套:fmt / clippy / check

bash 复制代码
cargo fmt                  # 统一格式(rustfmt),提交前必跑
cargo clippy -- -D warnings  # 静态检查(把警告当错误,CI 常用)
cargo check                # 只做类型检查,秒级反馈,比 build 快(迭代期主力)
bash 复制代码
# 本地提交前的标准动作
cargo fmt --check          # 没格式化就失败
cargo clippy --all-targets -- -D warnings
cargo test
  • rustfmt 有默认风格,别纠结自己排版------cargo fmt 一把梭;
  • clippy 会提示大量"更 Rust 的写法"(如该用 iter().sum() 别手写循环、needless_range_loop 等),每次提示都读一读,是免费的进阶老师;
  • 编辑器里 rust-analyzer 已在做 check 级反馈,跑 clippy 是更深一层。

10.6 基础篇总验收:把前 9 课收进一个工程

作为基础篇收尾,请独立完成一个综合练习工程(本次是"整理 + 测试",不是新知识):

需求:单词统计 CLI 库化

  1. cargo new word_counter;新建 src/lib.rs,把 models.rstextproc.rs 两个模块挂进去;
  2. textproc:把第 9 课的 top_words 挪进来并 pub,入参 &str + 返回 Vec<(String, usize)>
  3. modelspub struct WordStat { pub word: String, pub count: usize },并 impl From<(String, usize)> 便于转换(练 From);
  4. src/main.rs 改为 use word_counter::textproc::top_words;std::env::args 读一个文本路径或字符串参数并打印 Top5;
  5. 写单元测试:空串、纯空白、大小写归一、中文分词(split_whitespace 对中文按空格分,行为要写清楚在测试注释里)、TopN 截断;
  6. tests/ 下加一个集成测试,从外部 crate 调用 top_words 验证公开 API;
  7. cargo fmt + cargo clippy -- -D warnings + cargo test 全部通过。

验收门禁(基础篇毕业自检) :无需翻笔记,能在 30 秒内写出------mod/use/pub 的作用;lib.rsmain.rs 的职责分工;cargo test 三兄弟的运行方式;Cargo.lock 对 bin 项目为什么要提交;一条"改动前必跑"的质量命令链。

✅ 本节小结

  • 模块 :目录即模块、mod 声明 / use 引入 / pub 放行;crate::super::、相对 use 三种路径;默认私有=封装边界;
  • 工程结构 :lib + 薄 main;pub API 与私有实现分离;
  • 依赖Cargo.toml 宽约束 + Cargo.lock 钉精确版本(bin 提交 lock);semver 与 cargo update[profile.release][features]、workspace 概念;
  • 测试 :单元测试(#[cfg(test)] + #[test] + 断言宏 + #[should_panic] + Result 返回)、集成测试(tests/ 只能碰公开 API,故逻辑进 lib)、文档测试(注释代码块即用例);
  • 质量链fmt → clippy -D warnings → test,提交前必跑;
  • 毕业标准:能独立把一个多文件逻辑库 + 薄 CLI + 三层测试组织成可提交的工程。

基础篇结束语 :到这里你已经拥有 Rust 的完整"语言地基"------变量与类型、所有权与生命周期、struct/enum、trait/泛型、迭代器、错误处理与工程化。下一阶段进入进阶篇,第 11 课从智能指针与 interior mutability 开始,你会逐渐看到"语言特性如何在真实系统设计里被组合使用"。

相关推荐
qq_452396231 小时前
第九篇:《并发编程:线程、Channel 与共享状态》
rust
Source.Liu2 小时前
【A11】 labelprinter(Tauri 版)新建步骤
windows·rust
孙启超3 小时前
【AI开发之Rust】第 8 课:泛型、trait 与 trait 对象
开发语言·后端·rust
蓝宝石的傻话3 小时前
MiBee Eye Notebook:把闲置笔记本变成一台带麦克风的网络摄像头
数码相机·rust
小灰灰搞电子4 小时前
Rust Once 、OnceLock、LazyLock 一次性初始化详解
开发语言·后端·rust
qq_452396235 小时前
第八篇:《智能指针与内部可变性:Rust 的进阶内存管理》
rust
Amos_Web6 小时前
Rspack 源码解析(七):Module、Chunk 与加载树优化
前端·rust·源码阅读
QUOR7 小时前
ZorvAI APK 插件框架 · 技术架构与功能介绍
github·apk·编程语言