国内某私募中频组新加入的应届工程师周一第一天克隆了项目仓库。仓库的 Cargo.toml 声明一个 library crate 加一个 binary crate; src/lib.rs 暴露一个 pricing 模块; src/pricing.rs 装着 Lesson 2 写过的 510300.SH 沪深300 ETF 期权闭式 Black-Scholes call 定价; 顶层 tests/ 目录里有三条集成测试针对 library 的公开 API; CI 跑 cargo fmt --check、cargo clippy -- -D warnings、cargo test。这位新人的日常循环就是你接下来整个职业生涯都会敲的循环: 改一个文件、cargo check、cargo test、cargo fmt、提交。本课收尾 Module 3.5.1, 把一个真实 Rust 项目 (而不是单 src/main.rs 文件) 的样子讲清——模块系统、带依赖的 Cargo manifest、单元测试与集成测试、日常质量工具链巡游。
模块系统
Rust 的模块是「命名空间与可见性」机制, 不是构建系统也不是文件布局——Cargo 管构建, 模块管「谁能在哪里看到谁」。内联模块: mod name { ... items ... }。文件型模块: 父位置写 mod name;, rustc 优先找 src/name.rs, 然后再找 src/name/mod.rs (旧约定)。模块内的项默认是私有的; pub 向上一层公开: pub fn ...、pub struct ...、struct 字段也可逐个 pub。use crate::path::to::Thing 把 Thing 引入当前作用域以便不带前缀使用; use std::collections::HashMap 是标准库的对应写法。crate 关键字指当前 crate 的根, super 指父模块, self 指当前模块。
小项目的操作规则: 按逻辑领域声明一个子模块 (mod pricing;、mod parsing;)、每个子模块放一个 src/<name>.rs 文件、在 lib.rs 顶部用 pub use 把 binary crate 需要的少数几项重新导出。
library crate 与 binary crate
一个 Cargo 包 (package) 如有 src/lib.rs 就有一个 library crate, 如有 src/main.rs 就有一个 binary crate, 两者可以并存——这种情况下 binary 通过 use <crate_name>::... 依赖 library, 其中 <crate_name> 就是 Cargo.toml 的 [package] name = ...。library 是你能 publish 到 crates.io 让别人依赖的; binary 是你能作为可执行文件分发的。本课的 worked example 两者都用: library 装 pricing::black_scholes_call 与单元测试; binary 在 main 里调 library。多 binary 用 src/bin/<name>.rs 这里点一句, forward-pointer 到 3.5.4; Cargo 工作空间——顶层 [workspace] 表绑定多个 crate——forward-pointer 到 3.5.4 与 3.6.5。
Cargo.toml
worked-example 项目的完整 manifest, 见下面 Fenced TOML 块:
[package]
name = "hs300_pricer"
version = "0.1.0"
edition = "2021"
[dependencies]
approx = "0.5"
[dev-dependencies]
[package] 节设 name、version、edition。[dependencies] 节列运行时依赖; approx = "0.5" 拉取 approx crate 的 >= 0.5.0, < 0.6.0 区间版本 (caret semver 是默认的, "0.5" 是 "^0.5" 的简写)。[dev-dependencies] 节这里为空, 但 proptest、criterion 这类「仅测试时需要」的 crate 都放这里; 这里声明的依赖只在构建测试时链接, 下游用户使用你的 library 时永远看不到。[build-dependencies] 节为 build.rs 构建脚本服务, forward-pointer 到 3.5.4。
Cargo.lock 记录依赖树中每个 crate 的精确解析版本: binary crate 要 commit 它, 让 CI 与开发机的构建可比特对位; library crate 不 commit 它, 让下游消费者按自己的最新兼容版本解析。cargo build 头一次做解析、填充 target/ 目录, 后续构建走缓存——一个全新 crate 索引下的首次构建可能要一两分钟, 之后增量改动通常亚秒级。
src/lib.rs
//! 区域锚定的欧式 call 定价器。
pub mod pricing;
//! 起一段 crate 级 doc-comment; 空行隔开后 pub mod pricing; 声明并公开 pricing 子模块。library 入口就这几行, 真正的函数在另一文件里。
src/pricing.rs
/// 计算 Black-Scholes 欧式 call 价。
///
/// # Arguments
/// * `s` - spot price
/// * `k` - strike price
/// * `r` - risk-free rate (continuously compounded)
/// * `sigma` - volatility
/// * `t` - time to expiry in years
pub fn black_scholes_call(s: f64, k: f64, r: f64, sigma: f64, t: f64) -> f64 {
// implementation from L2
0.0
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
#[test]
fn test_call_atm() {
let price = black_scholes_call(4.20, 4.20, 0.025, 0.22, 0.25);
assert_abs_diff_eq!(price, 0.1854, epsilon = 1e-4);
}
}
/// 是 doc-comment, 由 cargo doc --open 渲染成 HTML; # Arguments 是参数列表的惯例小标题。mod tests 上的 #[cfg(test)] 属性意为「只在 cargo test 时编译此块」——测试代码永远不会进生产二进制。块内的 use super::*; 把父模块所有项 (含私有项, 这才是单元测试的意义——能直接测私有 helper) 引入。每个测试函数前面打 #[test]。approx::assert_abs_diff_eq!(price, 0.1854, epsilon = 1e-4) 替代天真的 assert_eq!(price, 0.1854)——永远不要对 f64 用 assert_eq!, 浮点相等与数值相等是两件事, epsilon = 1e-4 才是「精确到四位小数」的正确写法。
src/main.rs
use hs300_pricer::pricing::black_scholes_call;
fn main() {
let price = black_scholes_call(4.20, 4.30, 0.025, 0.22, 0.25);
println!("price = {:.4}", price);
}
use hs300_pricer::pricing::black_scholes_call; 从 library crate (Cargo.toml 里 name 为 hs300_pricer) 里把函数引入; binary 在 main 里调它。数值输入沿用 L2 的 510300.SH 沪深300 ETF 期权场景。cargo run --release 跑出与 L2 二进制相同的答案——但现在以 library 形态组织, 任何地方都能调、任何地方都能测。
日常 Cargo 循环, 加大版
cargo new --lib hs300_pricer
cargo build
cargo test
cargo fmt
cargo clippy -- -D warnings
cargo doc --open
cargo run --release
cargo new --lib 初始化一个纯 library 项目; 加 src/main.rs 就变成 library + binary 双结构。cargo build 解析依赖并编译。cargo test 编出测试二进制并跑所有 #[test] 函数, 覆盖单元测试、集成测试与 doctest 三层, 输出分三段。
集成测试
集成测试住在项目顶层的 tests/ 目录, 一个测试目标一个文件。每个文件 单独 编成一个二进制, 依赖 library crate、只能访问其 公开 API——这就是外部用户能触及的表面。示例 tests/pricing_integration.rs:
use hs300_pricer::pricing::black_scholes_call;
use approx::assert_abs_diff_eq;
#[test]
fn integration_call_price_is_positive_for_atm() {
let price = black_scholes_call(4.20, 4.20, 0.025, 0.22, 0.25);
assert!(price > 0.0);
assert_abs_diff_eq!(price, 0.1854, epsilon = 1e-3);
}
经验法则: 测试需要触碰私有 helper 就是单元测试 (与代码同文件、use super::*; 进模块内部); 测试 end-to-end 走 library 的公开 API 就是集成测试 (在 tests/)。两层叠加, 你能放心重构内部 helper, 集成测试钉死公开契约。
日常质量工具链
cargo fmt (底层调 rustfmt) 自动应用项目风格。默认风格是社区标准; 项目级微调写在项目根 rustfmt.toml。每次 commit 前跑一次 cargo fmt; CI 上跑 cargo fmt --check, 有任何需要重排的地方就失败构建。
cargo clippy 是 lint 工具。它检查上百种「编译器不抱怨但代码评审会抱怨」的模式: 冗余克隆、低效写法 (例如 vec.iter().count() 而非 vec.len())、可疑的 cast 等等。CI 日常调用 cargo clippy -- -D warnings——把所有 warning 当 error。文件 / 项目级 lint 可以用 #[allow(clippy::lint_name)] 静默, 但要有充分理由——理解 lint 之前不要伸手关它。
cargo doc --open 从 /// doc-comment 生成 HTML 并在浏览器打开。doc-comment 里的 fenced ```rust 代码块 也会 被 cargo test 当 doctest 运行——标准库自己就这么做, 生产 Rust 代码也该这么做。给 black_scholes_call 加一段四行的 doc-comment, 跑 cargo doc --open 看渲染页, 顺手让 doctest 把文档钉准。
行业收束
国内头部基础设施与中频私募团队的 Rust 项目 CI 流水线几乎都收敛到同一套规约: 在多套环境下分别用最小核数与生产核数各跑一次, 把可重现性与并发隐患都覆盖到。具体到子命令, PingCAP、蚂蚁链、字节火山引擎以及部分私募的 Rust 项目 CI 几乎清一色跑这三条: cargo fmt --check、cargo clippy --all-targets --all-features -- -D warnings、cargo test --all-features --workspace, 再额外加 cargo deny check 做 license / advisory / 禁用 crate 检查 (这块留到 3.6.2 Git & Code Quality)。本课的 worked example 在单 crate 尺度演示了前三条, 多 crate workspace 的扩展放到 Module 3.5.4。到这里你已经完成 Module 3.5.1: 能写、能编、能组织、能测、能格式化、能 lint、能生成文档一个 Rust 项目。Module 3.5.2 从并发开始。
练习
Exercise
拿本课的 worked-example 项目: (a) 在 src/pricing.rs 里加第二个公开函数 pub fn black_scholes_put(s: f64, k: f64, r: f64, sigma: f64, t: f64) -> f64 计算欧式 put 价 (直接闭式公式或由 call 价走 put-call parity 都行), 配同款 # Arguments 的 /// doc-comment; (b) 在已有的 #[cfg(test)] mod tests 块里给 put 加三条单元测试 (deep-ITM、ATM、deep-OTM); (c) 在 tests/put_call_parity.rs 加一条集成测试, 用 assert_abs_diff_eq! 断言 call_price - put_price 等于 s - k * (-r * t).exp() 在 epsilon = 1e-6 内; (d) 依次跑 cargo fmt、cargo clippy -- -D warnings、cargo test, 报告这三条中哪几条在成功前产生了输出; (e) 故意把 put 公式里某个 + 写成 -, 再跑 cargo test, 逐字抄下失败断言信息, 然后改回来确认全绿。
提示
提示
cargo fmt 在已格式化的代码上零输出; cargo clippy 干净时不打 warning; cargo test 打三段汇总。代码干净时 test result: ok 之前没有诊断输出。