小卡第一天到「Rust 餐厅」后厨上班,师傅丢给他一句话:「你只管炒菜,杂事都交给 Cargo。」Cargo 是这家餐厅的后厨总管:他有一本万能菜单,记着每道菜需要哪些半成品料理包;他每天清晨骑着三轮车去中央食材市场(crates.io)进货,哪个批次要用哪个版本,他门儿清;炒完菜他让试菜员尝一口(cargo test),合格才端出去;餐厅开了七家分店,他让所有分店共用一间中央厨房(workspace),原料口味完全一致;小卡研究出的秘制酱料,经他包装后还能上架到美食平台,供全世界的餐厅下载使用。整个故事里,小卡只需要记住一件事:专注写代码。接下来,我们跟着小卡,把这套后厨管理术一步步学明白。
学会 Cargo,等于解锁了 Rust 开发的「自动驾驶」:从建项目、管依赖、跑测试到发布上线,一套命令全搞定。这是每个 Rust 工程师的日常基本功,也是进入现代 AI 基础设施开发(如推理引擎、高性能服务)的必备技能。
目录
可视化演示
演示Cargo依赖解析与版本锁定、构建流水线编译过程、workspace多crate共享管理,交互切换步骤查看核心机制。
本节配有交互动效(进入视口后自动加载,约 3KB)。
依赖解析与版本锁定 Cargo.toml声明依赖版本范围 解析器解决传递依赖冲突 semver: ^1.2 → ≥1.2.0,<2.0.0 Cargo.lock锁定精确版本保证可复现构建
认识 Cargo:后厨总管的第一课
小卡第一天上班,Cargo 递给他一张员工卡,刷卡动作是 cargo new my_dish。刷完,一间小厨房拔地而起:墙上挂着菜单(Cargo.toml),灶台设在 src/main.rs,仓库间是 target/ 目录。小卡还没动一根手指,一个整洁的项目骨架就自动站在眼前——这就是「后厨总管」给的见面礼。
Cargo 是 Rust 官方的构建系统与包管理器,名字取「货物」之意,寓意把代码当货物来打包、运输。在 Rust 世界里,几乎所有日常操作都围绕 Cargo 展开:它是统筹全局的后厨总管,把建目录、下依赖、编代码、跑测试、发布上架整合成一条流水线,开发者只需要记住几条命令就能运转整个厨房。
先看项目结构:Cargo.toml 是菜单加采购单,声明了菜品信息和所需食材;src/main.rs 是主灶台(可执行程序的入口),如果想写库则在 src/lib.rs;target/ 是成品仓库——所有编译产物都堆在这里,删掉也无妨,Cargo 可以重新做一桌。创建项目用 cargo new(全新开一间厨房)或 cargo init(在现有空房间里布置厨房)。日常最常用的四道命令是:cargo build 做菜、cargo run 上菜、cargo check 快速验味、cargo test 全量试菜。
小卡看着自动生成的整洁厨房,第一次体会到「总管家」的含金量:以前在别的语言里手忙脚乱搭环境的日子,一去不复返了。
cargo new 后,Cargo 自动生成一张菜单、一个灶台文件和一个版本管理忽略清单。- 输入命令
cargo new是开新厨房的指令,后面通常跟项目名。 - 生成骨架文件命令自动产出三个核心文件,不需要手动创建。
- 花括号集合含义数学中的花括号表示「一组对象」,这里表示这些文件共同构成项目的最小骨架。
项目配置:Cargo.toml 是菜单兼采购单
每个后厨墙上都挂着一本「菜单 + 采购单」的合订本,这就是 Cargo.toml。客人点什么菜(依赖什么库)、采购员进什么货(依赖版本范围)、能不能加辣(可选特性),全都明明白白写在上面。小卡第一次翻开它,发现整间厨房的规矩都浓缩在这一个文件里。
Cargo.toml 使用 TOML 格式书写——一种专门给人类读的配置语言,结构像字典条目一样清晰,用方括号表示区块。它不像代码那样需要编译,改完立即生效,Cargo 总管会实时照单执行。
文件内最常打交道的有三块:[package] 区块登记菜品的元信息——菜名(name)、菜谱版本(version)、以及基于哪一年的 Rust 口味(edition);[dependencies] 区块是采购清单,列出所有第三方料理包及版本要求;[features] 区块定义可选配项,比如「加辣」「加蛋」,别家在引用这个库时可以按需开启。还有一个 [profile] 区块用来调整做菜档位:dev 档优先出锅速度,release 档优先菜品质量(运行性能)。
小卡第一次改菜单就发现,改完 Cargo.toml 后,总管自动生成了一张叫 Cargo.lock 的采购订单——版本被精确锁死,从此每桌菜的配方都一模一样。
- package 区块登记菜名、版本号、Rust 版本年份等元信息,相当于菜谱的封面。
- dependencies 区块列出所有第三方料理包的名称与版本约束,相当于采购单。
- features 区块定义可选项,让下游使用者按需开启,相当于「加辣/不加辣」的勾选框。
依赖管理:精确到批次的采购哲学
进货最怕供应商偷偷改配方:今天买的酱油和昨天的味道不一样,整道菜就毁了。Cargo 的采购逻辑是,下单前先问清楚三个数字——主版本、次版本、补丁版本,再决定买哪批货。
这套编号规则叫语义化版本,格式是 主版本.次版本.补丁版本,例如 1.2.3。主版本号(1)变了说明有破坏性改动,老顾客可能吃不了;次版本号(2)变了说明加了新菜但不影响老味道;补丁版本号(3)变了只是修了小 bug,安全得很。
在 Cargo.toml 里写依赖约束时,最常见的写法是 ^1.2.3(读作 caret 约束):它允许解析到 1.x.x 系列中不低于 1.2.3 的任意版本,但绝不跨过 2.0.0。这种「开放但守底线」的策略,既保证了灵活升级,又防止破坏性版本悄悄混入。代码跑起来后,Cargo 会生成 Cargo.lock 文件,把最终解析出的精确版本号全部记录在案——这就是那张「精确到批次」的采购订单。同时,依赖还有传递性:你直接依赖 A,A 又依赖 B,Cargo 会把 B 也自动拉进来解析,像总管查供应商的供应商。
小卡盯着 Cargo.lock 里那串精确到第三位的版本号,恍然大悟:所谓「可复现构建」,就是无论什么时候重进后厨,总管都拿出同一批货,做出同一桌菜。
- 主版本号必须相同
^1.2.3只接受 1.x.x,绝不允许 2.0 悄悄混入,避免破坏性变更。 - 次版本号不低于下限只要新版本的功能比要求的多,就可以接受。
- 补丁版本自由浮动小修小补的版本最安全,Cargo 会自然选择最新的补丁号。
crates.io:中央食材市场
后厨再大,也不可能自己种所有菜、酿所有酱。Rust 社区的解法,是开一家官方中央食材市场——crates.io。货架上摆着几十万种半成品料理包,小卡需要什么,推着小车去进货就行。
这里的「料理包」在 Rust 里叫 crate,是包(package)和库(library)的统称。一个 crate 就是一份打包好的、带说明书(文档)的代码,有的负责序列化数据(如 serde),有的负责异步运行时(如 tokio)。crates.io 是 Rust 官方的包注册中心,相当于市场的管理方,所有 crate 在这里登记、存档、分发。
进货动作极其简单:cargo add serde 一条命令,Cargo 总管就替小卡写好 Cargo.toml 里的依赖声明,并立刻下载到本地;cargo search 关键词 可以在市场里搜货。下载下来的 crate 源码会进入本地的缓存仓库,编译时再引用。国内开发者如果觉得市场太远(网络慢),还可以配置镜像源,相当于在市场旁边开一个本地分仓库,把热门货提前囤好。
小卡用 cargo add serde 三秒进完货,忍不住感叹:以前在其他语言里手动找包、下载、配置路径的日子,跟现在一比简直是原始社会。
- 版本号唯一市场上不允许出现两个同名同版本的 crate,否则下游会分不清该买哪个。
- 许可证声明每个 crate 必须声明开源许可证,相当于食品包装上的配料公示。
- 打包验证通过
cargo package会在本地模拟打包,确保上架后别人能正常下载使用。
构建与测试:后厨流水线
食材备齐,后厨开火。Cargo 的构建流水线像极了做饭:洗菜切配(编译)、装盘(链接)、出锅(生成可执行文件)。小卡最开心的,是这套流水线全自动——他只需喊一嗓子「上菜」,总管就指挥全员动起来。
cargo build 是标准做菜流程,编译当前项目和所有依赖,产出可执行文件;加 --release 参数则切换到「宴席档」,编译器会花更多时间深度优化,做出的菜运行更快,适合端给客人(交付上线)。日常开发则用 dev 档,出锅快、调试信息全。
开发中最常用的其实是 cargo check——它只检查类型和借用关系是否正确,不生成机器码,速度比完整编译快数倍,好比拿筷子蘸一下尝尝咸淡,不必等整桌菜做完。而真正的质量把关靠 cargo test:代码中用 #[test] 标记的函数就是试菜项目,测试分三层——单元测试(灶台上试小样)、集成测试(整桌菜试吃)、文档测试(说明书里的示例也要能跑通)。
小卡从此养成习惯:改完代码先 cargo check 快速验味,确认无误再 cargo test 正式试菜,最后才敢 cargo run 端上桌——极少翻车。
- 编译每个 crate对第 i 个 crate 执行编译,产出中间目标文件,相当于把每样食材切好备好。
- 并集合并把所有编译产物放进同一个集合,任何依赖缺失都会在这一步暴露。
- 链接成二进制Link 操作把所有目标文件拼装成最终可执行文件,像是把切好的菜摆盘上桌。
工作空间:连锁餐厅的中央厨房
餐厅生意越做越大,小卡开了七家分店,每家都有独立的厨房(crate)。如果每家都单独买菜、单独建库,浪费不说,口味还容易跑偏。Cargo 给出的解法是 workspace——工作空间,相当于一间中央厨房,所有分店共用一套采购订单和一口大锅。
workspace 是 Cargo 管理多个相关 crate 的方式。在根目录的 Cargo.toml 里声明 [workspace] 区块并列出成员,这些成员便纳入统一管理。最大的好处是共享一份 Cargo.lock:所有成员依赖的第三方包版本全局一致,不会出现「A 店用 serde 1.0、B 店用 serde 2.0」的混乱。成员之间用 path 依赖直接互相引用,改公共代码立刻联动。
实际工程里,workspace 常用于把大型项目拆成多个小 crate:一个 common 放公共工具,一个 api 提供服务接口,一个 cli 放命令行入口。每个 crate 保持独立可测试,合在一起又是一个整体。小卡把秘制酱料配方抽成 common 包后,三家分店共用一处配方,改一次全店生效——这正是 workspace 的核心价值。
小卡站在中央厨房里看着七家分店同时出菜,感慨万千:一个人管七个厨房听起来可怕,但在 Cargo 的 workspace 面前,不过是点一下 cargo build 的事。
- 成员 crate每个 Member 是一个独立的后厨灶台,有自己的 Cargo.toml 和源码目录。
- 并集成组所有成员拼在一起构成完整的大项目,互不冲突。
- 共享锁文件整个 workspace 只有一份 Cargo.lock,确保所有分店菜谱完全一致。
发布流程:菜谱上架到全球
小卡自创的「内存安全酱」在自家餐厅大受欢迎,师傅说:上架到美食平台吧,让全世界餐厅都能用。这就是 Rust 世界的 cargo publish——把 crate 发布到 crates.io,供全球开发者下载。
发布前要过三道关:版本号必须符合语义化规则且未被占用;必须声明许可证;必须通过 cargo package 本地模拟打包,确保包内容完整、能正常使用。这些都通过了,才执行 cargo publish 正式上架。如果发现某个版本有严重 bug,可以用 cargo yank 把该版本「下架」,阻止新项目依赖它,但已锁定的项目不受影响——好比召回一批有问题的酱油,但已经上桌的菜不收回。
版本迭代同样有规矩:修 bug 升级补丁号(1.2.3 → 1.2.4),加新功能、保持兼容升级次版本号(1.2.3 → 1.3.0),破坏性改动才升级主版本号(1.2.3 → 2.0.0)。这套规则是对下游的承诺:看到主版本没变,大家就敢安心升级。
上架一周后,小卡的酱料被下载了上百万次,他成了后厨圈的明星。回望这趟旅程,从建厨房、买菜、炒菜、试菜到开分店、上架全球——所有杂事,都是那位叫 Cargo 的总管在背后默默操持。
- patch 递增只修 bug,第三位加 1,对下游完全安全。
- minor 递增新增功能且保持兼容,第二位加 1,下游可放心升级。
- major 递增出现破坏性改动,第一位加 1,下游需要仔细阅读升级说明。
AI 应用场景
场景一:AI 推理引擎的依赖治理
阶段:模型部署与工程化
解决的问题:AI 推理服务往往同时依赖高性能计算库、分词器、HTTP 框架等十几个底层 crate,版本错配会导致「编译十分钟后报错」的噩梦。Cargo 用 Cargo.lock 精确锁定每个依赖版本,配合 CI 流水线实现一键复现构建,从源头消灭「在我机器上是好的」这类问题。
具体案例:某 NLP 团队用 Rust 开发高性能打分服务,依赖 candle(LLM 推理库)、tokenizers(分词器)、axum(HTTP 服务)。团队在 CI 中依次执行 cargo fmt、cargo clippy、cargo test、cargo build --release,任何依赖升级必须先过全量测试再合入主干。
场景二:Monorepo 大型 AI 平台的 workspace 拆分
阶段:软件架构与团队协作
解决的问题:AI 平台常包含数据管道、模型服务、特征存储等模块,若全部塞进一个 crate,编译缓慢、耦合严重。工作空间 workspace 让各模块独立成 crate,共享一份依赖锁文件,公共逻辑抽成 common 成员,一处修改全链路生效。
具体案例:某推荐系统团队维护一个含 12 个 crate 的 workspace:common(公共工具)、feature_server、rank_service、offline_pipeline 等。每次 cargo build 一次性构建全部成员,CI 中任一 crate 测试失败都会阻断发布。
场景三:Rust 版 AI 开源库的分发
阶段:开源社区与生态建设
解决的问题:研究者写好一个 Rust 版模型推理库,若手动分发源码包,用户安装成本极高。通过 cargo publish 发布到 crates.io,全球开发者一条 cargo add 命令即可拉取,传递依赖自动解析,文档示例还能通过文档测试自动验证。
具体案例:HuggingFace 的 candle、rust-bert 等 Rust AI 库都通过 crates.io 分发,严格遵循语义化版本:新模型支持升 minor,破坏性 API 变更升 major,让下游企业敢大胆升级。
Python 演示
from collections import deque # 用字典模拟 Cargo.toml:包名 -> 版本号 + 依赖清单(约束)cargo_toml = { "my_app": {"version": "0.1.0", "deps": {"serde": "^1.0", "tokio": "^1.20"}}, "serde": {"version": "1.0.200", "deps": {"serde_derive": "^1.0"}}, "serde_derive": {"version": "1.0.200", "deps": {}}, "tokio": {"version": "1.35.0", "deps": {"bytes": "^1.0"}}, "bytes": {"version": "1.5.0", "deps": {}},} def parse_version(v): return tuple(int(x) for x in v.split(".")) def match_constraint(constraint, version): # 实现 caret(^) 约束: 主版本相同且不低于下限 if constraint.startswith("^"): lo = parse_version(constraint[1:]) v = parse_version(version) return v[0] == lo[0] and v >= lo return version == constraint def resolve(root): # BFS 收集所有传递依赖, 得到精确锁定计划 plan = {} queue = deque([root]) while queue: name = queue.popleft() if name in plan: continue meta = cargo_toml[name] for dep, req in meta.get("deps", {}).items(): v = cargo_toml[dep]["version"] if match_constraint(req, v): plan[dep] = v queue.append(dep) plan[root] = cargo_toml[root]["version"] return plan def build_order(plan): # 拓扑排序: 依赖别人的先编译, 被依赖的后编译 indeg = {name: 0 for name in plan} for name in plan: for dep in cargo_toml[name].get("deps", {}): if dep in plan: indeg[dep] += 1 ready = deque([name for name in plan if indeg[name] == 0]) order = [] while ready: name = ready.popleft() order.append(name) for dep in cargo_toml[name].get("deps", {}): if dep in plan: indeg[dep] -= 1 if indeg[dep] == 0: ready.append(dep) return order if __name__ == "__main__": lock = resolve("my_app") print("Cargo.lock resolved:") for name, version in sorted(lock.items()): print(" {} = {}".format(name, version)) print("Build order:", " -> ".join(build_order(lock)))
cargo_toml = {...}用字典模拟 Cargo.toml,记录每个包自己声明的版本和对下游的约束。def match_constraint(...)实现 caret(^)版本约束:主版本相同且版本号不低于约束下限。queue = deque([root])BFS 广度优先遍历,像 Cargo 一样逐层收集所有传递依赖。for dep, req in meta.get("deps", {}).items():遍历每个包的依赖清单,逐条核对能否满足约束。plan[dep] = v把通过约束检查的精确版本写入plan字典,相当于生成 Cargo.lock。if indeg[dep] == 0:当某个包的所有依赖都已就绪,它才进入构建队列。print("Build order:", ...)输出与 Cargo 一致的从依赖底层到顶层的构建顺序。
常见误区
误区Cargo 不就是从网上下载库的工具吗?下载完就没它什么事了,编译是编译器 rustc 的活。
正解Cargo 是「全流程后厨总管」:它负责建项目、下依赖、编译调度、跑测试、发布上架,下载只是它日常工作的一小环。cargo build 背后是 Cargo 自动调用 rustc、管理编译顺序、缓存增量产物。它更像 npm + make + pytest 的一体化综合体,而不是单纯的下载器。
为何常见:新手最先接触 cargo add 或 cargo build 时总看到它在联网拉包,于是把「拉包」当成了全部职责。
误区Cargo.lock 是自动生成的临时文件,应该加进 .gitignore 里,不用管它。
正解这得看项目类型。二进制应用(能跑的程序)必须提交 Cargo.lock,保证每次发布构建的依赖完全一致,这是可复现部署的基石;但库项目(给别人用的 crate)通常不提交,因为库需要在下游更宽的版本范围内解析,提交反而会限制兼容性。一句话:应用锁配方,库留灵活。
为何常见:Cargo.lock 确实由工具自动生成,容易被误认为「垃圾产物」,而实际上它是保证「同一批货做同一桌菜」的定海神针。
误区两个都是编译命令,结果应该一样,随便用哪个都行。
正解两者差异巨大。cargo check 只做类型检查和借用检查,不生成机器码,速度比完整编译快数倍,适合开发中高频验证;cargo build 才真正产出可执行文件。类比来说,check 是拿筷子蘸一下尝咸淡,build 是整套上菜流程。把 check 当 build 用会拿不到程序,把 build 当 check 用会浪费大量时间。
为何常见:两条命令语法几乎一样、输出也都很快,新手容易误以为 check 只是 build 的加速模式,而忽略它「只验味不出菜」的本质。
误区版本号嘛,从 0.0.1 开始,改一个 bug 就加一位,全凭心情。
正解Rust 生态默认语义化版本:主版本(major)代表破坏性变更、次版本(minor)代表新增兼容功能、补丁版本(patch)代表修 bug。随意乱升会导致依赖方在 ^1.0.0 约束下解析到不兼容的新版本,运行时直接崩溃。这就好比悄悄把菜谱里的主料换了,却跟客人说「配方没变」——信任就是这样崩掉的。
为何常见:没经历过下游项目被 breaking change 破坏的痛苦,就难以理解版本号本质上是「对兼容性的承诺」。
面试题
Q1. Cargo 中用来在空目录里初始化一个新项目的命令是?
- A.cargo new
- B.cargo init← 正确答案
- C.cargo build
- D.cargo run
答案:B。cargo init 在已有空目录中初始化项目;cargo new 是新建一个全新目录并在其中初始化。build 和 run 分别是编译和运行,不是初始化命令。类比:init 是在现有厨房里布置灶台,new 是连厨房一起盖好。
Q2. Cargo.toml 中 [dependencies] 区块的作用是?
- A.声明项目名和版本号
- B.列出项目依赖的第三方 crate 及版本约束← 正确答案
- C.配置编译优化等级
- D.定义单元测试函数
答案:B。[dependencies] 是采购清单,列出所有需要的料理包及版本要求。项目名和版本号在 [package] 区块,编译优化在 [profile] 区块,测试函数写在代码里而非 Cargo.toml 中。这道题考的正是 Cargo.toml 各区块的职责分工。
Q3. 关于语义化版本 1.2.3,其中「3」代表什么?
- A.主版本号,表示破坏性变更
- B.次版本号,表示新增功能
- C.补丁版本号,表示 bug 修复← 正确答案
- D.构建流水线编号
答案:C。语义化版本格式为「主。次。补丁」:主版本破坏性变更,次版本新增兼容功能,补丁版本只修 bug。「3」是第三位,即补丁号。把它理解为「换菜谱主料 / 加新菜 / 修调味」三种改动程度的等级即可。
Q4. 哪个文件记录了最终锁定后的精确依赖版本,保证每次构建结果一致?
- A.Cargo.toml
- B.Cargo.lock← 正确答案
- C..gitignore
- D.target/config.json
答案:B。Cargo.lock 是「精确到批次的采购订单」,记录解析出的精确版本号。Cargo.toml 只写版本范围(如 ^1.0),不锁定具体版本;.gitignore 管理版本控制忽略规则;target 目录是编译产物,其中没有 config.json 这类文件。
Q5. 依赖约束 ^1.2.3 允许解析到下列哪个版本?
- A.0.9.0
- B.2.0.0
- C.1.9.0← 正确答案
- D.1.2.2
答案:C。caret(^)表示「主版本相同且不低于下限」:^1.2.3 接受 >=1.2.3 且 <2.0.0 的版本。1.9.0 满足条件;0.9.0 主版本不对,2.0.0 跨了主版本,1.2.2 低于下限。这就像「同一个菜系系列,但不能换主料」,Cargo 会选范围内最新的合规版本。
Q6. 关于 Cargo.lock 的提交策略,下列说法正确的是?
- A.二进制项目与库项目都应提交 Cargo.lock
- B.二进制项目与库项目都不应提交 Cargo.lock
- C.二进制项目应忽略 Cargo.lock,库项目应提交
- D.二进制项目应提交 Cargo.lock,库项目通常忽略← 正确答案
答案:D。二进制应用提交 lock,确保部署时可复现同一套依赖;库项目通常忽略 lock,以便在下游更宽的版本范围内解析,提升兼容性。类比:自营餐厅要锁定进货批次保证口味一致,半成品供应商则要适应不同买家的厨房条件。
Q7. 下列哪些属于 Cargo 的常用命令?
- A.cargo build← 正确答案
- B.cargo test← 正确答案
- C.cargo pip
- D.cargo check← 正确答案
答案:ABD。build(编译)、test(测试)、check(快速检查)都是 Cargo 内置命令。cargo pip 并不存在——pip 是 Python 的工具,Cargo 生态中下载依赖用 cargo add。这道题提醒我们别把其他语言的包管理命令混淆进 Cargo。
Q8. 关于 workspace(工作空间)的描述,正确的有哪些?
- A.多个成员 crate 共享一份 Cargo.lock← 正确答案
- B.每个成员 crate 必须有独立的 Cargo.lock
- C.成员之间可以用 path 依赖互相引用← 正确答案
- D.workspace 中的所有成员必须包含相同代码
答案:AC。workspace 的价值正在于共享一份 Cargo.lock,让所有成员依赖版本全局一致,因此 B 错误;成员之间通过 path 依赖互引是常规操作,C 正确;成员是独立的 crate,各自代码不同、职责不同,D 错误。类比中央厨房:各分店菜不同,但共用同一套进货订单。
Q9. cargo publish 发布 crate 到 crates.io 之前,以下哪项不是必要检查?
- A.版本号在 crates.io 上未被占用
- B.已声明许可证(license)
- C.通过 cargo package 本地打包验证
- D.已上传编译好的二进制文件到 crates.io← 正确答案
答案:D。crates.io 托管的是源码包,不直接接收编译好的二进制文件,下载后由使用方的 Cargo 负责编译。发布前必须保证版本唯一、license 已声明、打包验证通过。这好比美食平台上架的是「菜谱和半成品包」,而不是已经炒好的菜。
Q10. 在 Rust 大型项目中利用 Cargo 进行模块拆分,下列哪些做法合理?
- A.将公共逻辑抽成 workspace 中的 common 成员 crate← 正确答案
- B.不同业务模块通过 path 依赖引用 workspace 内的公共 crate← 正确答案
- C.为了避免依赖问题,把所有代码堆在一个超大 main.rs 中
- D.对每个成员 crate 分别执行 cargo test 验证其正确性← 正确答案
答案:ABD。A、B、D 都是 workspace 拆分的常规操作:公共逻辑下沉为 common,业务模块用 path 依赖它,各成员独立测试。C 是反面教材——巨无霸单文件会拖慢编译、增加耦合,违背 Cargo 的模块化设计初衷。