我原来以为,写 Rust 就是把 .rs 文件编译出来。
后来我发现,真正进入一个项目以后,事情完全不是这么简单。
我一开始其实不理解 Cargo
上一篇把 Rust 装好以后,我先做了一件最普通的事情:
写一个 main.rs。
fn main() {
println!("Hello, world!");
} 这件事情本身没有什么神奇的。
Rust 编译器 rustc 可以直接编译一个 Rust 源文件。官方 Cargo 文档也是从这里开始介绍的:一个 crate 最终由 rustc 编译,最简单的时候,甚至可以直接把文件交给 rustc。
所以我当时第一个疑问其实很朴素:
既然 rustc 已经能把代码编译出来了,那为什么 Rust 还需要 Cargo?
如果只是第一次写 Hello World,这个问题确实没什么感觉。
但只要项目稍微复杂一点,答案就开始出现了。
一个真正的项目不会永远只有一个 .rs 文件。
它可能有几十个模块,有外部依赖,有测试,有不同的构建配置,还要考虑不同机器上能不能得到一致的构建结果。
这时候,如果所有东西都靠自己调用 rustc,事情很快就会变得麻烦。
官方对 Cargo 为什么存在的解释其实很直接:直接使用 rustc 时,项目的编译参数、外部依赖以及依赖的依赖都需要自己处理;项目一复杂,手工管理这些东西就容易出错。
所以我后来才慢慢理解:
rustc 解决的是“怎么编译 Rust 代码”。
Cargo 解决的是“怎么管理一个 Rust 项目”。
这两个东西不是竞争关系。
Cargo 本身也不是替代 rustc 的另一个编译器。
它更像是站在编译器外面,把一个真实项目需要做的那些事情组织起来。
rustc main.rs 可以直接把 .rs 源文件编译成可执行文件。单文件时,一切都很简单。
那为什么还需要 Cargo?为什么官方教程几乎不让你直接调用 rustc?
Arustc
编译一个程序
BCargo
项目 · 依赖 · 构建 · 测试 · 发布
→结论
两者解决的是不同规模的问题
第一个 Cargo 项目
于是我开始创建自己的第一个 Cargo 项目:
$ cargo new hello_cargo $ cd hello_cargo
Cargo 会直接帮我生成一个基本项目:
hello_cargo/
├── Cargo.toml
└── src/
└── main.rs 这其实是我第一次明显感觉到:
哦,原来 Rust 的“项目”是有结构的。
官方 Cargo Book 目前的示例也是这个结构。cargo new 默认创建的是 binary package;如果要创建 library,则可以使用 --lib。
打开 src/main.rs,还是刚才那个 Hello world(没什么变化)。
真正让我开始产生疑问的是另外一个文件:
Cargo.toml
Cargo.toml 到底是什么?
第一次看到它的时候,我只知道:
这是 Cargo 的配置文件。
但这个解释其实没什么用。
因为真正重要的是:
Cargo 到底需要从这里知道什么?
一个刚创建出来的项目大概是这样:
[package]
name = "hello_cargo"
version = "0.1.0"
edition = "2024"
[dependencies] 这里其实已经透露了很多东西。
[package] 描述的是这个 package 本身。
比如 name = "hello_cargo" 和 version = "0.1.0",就是项目名称和版本。
而 [dependencies] 是告诉 Cargo:
这个项目依赖哪些外部 crate。
现在它还是空的,所以看起来没什么感觉。
但等以后开始使用 Tokio、Serde、Reqwest 之类的库,你会发现这个区域会变成 Rust 项目里非常重要的一部分。
Cargo 官方把 Cargo.toml 称为 manifest,也就是描述 package 所需信息的清单。
edition = "2024" 不是 Rust 版本
这里有一个我觉得初学 Rust 很容易误解的东西:
edition = "2024"
第一次看到它,我脑子里的反应其实是:
Rust 2024?那 Rust 现在到底是哪个版本?
后来才知道:
Edition 不是 Rust compiler 的版本号。
Rust 使用 edition 来组织一些语言和生态层面的变化。官方目前提供 Rust 2015、2018、2021 和 2024 editions;项目在 Cargo.toml 中通过 edition 指定自己使用哪一个 edition。
版本和 edition 是两回事:
ARust 版本
编译器本身的发布版本
Rust 1.90.0
BRust Edition
语言层面的变化机制
edition = "2024"
所以 edition = "2024" 更准确的理解是:
这个项目按照 Rust 2024 Edition 的规则和习惯来解释代码。
它和你安装的 Rust compiler 版本不是同一个概念。
而当前官方 Rust Book 本身也已经以 Rust 2024 Edition 为例,并要求使用较新的 Rust 工具链。
这个区别现在记住就够了。
以后真正遇到 Edition 之间的语言变化,再单独研究。
第一次 cargo build
项目已经有了。
接下来当然是:
$ cargo build Compiling hello_cargo v0.1.0 (file:///.../hello_cargo) Finished `dev` profile [unoptimized + debuginfo]
然后项目里多出来一个 target/:
target/
└── debug/
└── hello_cargo 这时候我才真正开始理解 Cargo 和 rustc 的区别。
之前直接运行 rustc main.rs,得到的结果就在当前目录附近。
现在 cargo build,Cargo 会按照项目结构管理构建结果,把开发构建的产物放到 target/debug。官方文档也明确把这种 Debug 构建作为默认的开发构建。
所以项目目录慢慢变成:
hello_cargo/
├── Cargo.toml
├── Cargo.lock
├── src/
│ └── main.rs
└── target/
└── debug/
└── hello_cargo 这已经和刚才那个只有一个 main.rs 的世界不太一样了。
cargo run:其实就是懒一点
如果只是想看看程序能不能跑:
$ cargo run Compiling hello_cargo v0.1.0 (file:///.../hello_cargo) Finished `dev` profile [unoptimized + debuginfo] Running `target/debug/hello_cargo` Hello, world!
Cargo 会帮你完成构建,然后运行生成的程序。
也就是说:
Acargo build
cargo build
更像:给我构建结果。
Bcargo run
cargo run
更像:构建一下,然后现在就跑起来。
官方文档也是这么定义 cargo run 的:它会编译 package,然后运行生成的 binary。
所以日常开发的时候,我其实很少需要自己去找 target/debug/hello_cargo,直接 cargo run 就够了。
这也是我觉得 Cargo 开始变得舒服的地方。
它没有改变 Rust 编译器本身。
它只是把很多重复的事情接起来了。
那 cargo check 又是干什么的?
这个命令一开始我反而觉得有点奇怪:
既然已经有 cargo build,为什么还需要一个 check?
后来发现这个命令其实非常实用。
cargo check 会检查当前 package 以及依赖是否存在编译错误,但会跳过最终的代码生成,因此通常比完整的 cargo build 更快。
简单理解,它就是先问一句“代码能不能通过编译检查?”,然后告诉我结果。它不会为了得到最终可执行文件而完成最后的代码生成。
所以在不断修改代码的时候,cargo check 非常方便:
官方 Rust Book 也特别推荐在开发过程中周期性运行 cargo check,因为它通常比 cargo build 更快。
当然,它也不是“比 build 更严格”或者“完全等价于 build”。
Cargo 官方文档特别提醒,有些只有在代码生成阶段才会出现的诊断,cargo check 本身不会报告。
所以我现在更愿意把它理解成:
我现在还没准备好要一个最终程序,但我想先知道这堆代码能不能通过编译检查。
Debug 和 Release
然后又遇到了两个目录:
target/debug/ 和 target/release/。
刚开始看名字就大概能猜到是什么意思。
开发的时候 cargo build,默认是 Debug 构建。
如果准备做 release build,用 cargo build --release,结果会进入 target/release/。
A开发阶段
cargo build → target/debug
编译快,未优化,适合开发调试
B发布阶段
cargo build --release → target/release
启用优化,编译更慢,但运行性能适合正式使用
两者的主要区别之一是优化。
官方 Cargo 文档说明,Debug 构建默认面向开发,编译时间更短,因为不会进行 release 构建中的优化;Release 构建会花更多时间编译,但生成的程序通常运行得更快。
现在我还不需要研究 Cargo profile 里面几十个配置项。
知道这两个目录分别是干什么的,已经足够。
然后我发现了 Cargo.lock
运行 Cargo 之后,你可能还会看到一个新的文件:
Cargo.lock
这个文件第一次看到的时候,我其实挺容易把它和 Cargo.toml 搞混。
因为它们都和依赖有关。
但两者的角色完全不同。
我现在会这么记:
ACargo.toml
我告诉 Cargo:
“我需要什么依赖、允许什么版本范围”
BCargo.lock
Cargo 告诉项目:
“这次解析之后,具体用了哪些版本”
官方文档的表述也很明确:
Cargo.toml 描述依赖的大致要求,由开发者编写;Cargo.lock 保存精确的依赖信息,由 Cargo 维护。
这背后其实有一个非常现实的问题。
假设我的项目依赖一个库。
今天我构建的时候拿到的是某个版本。
过几个月,那个库出了新版本。
如果没有锁定信息,那么不同时间、不同环境重新解析依赖时,就可能得到不同的依赖组合。
Cargo.lock 的作用之一,就是把 Cargo 当时解析出来的具体依赖版本记录下来,从而帮助项目获得更可重复的构建结果。
所以它不是一个“我要不要手动维护的配置文件”。
它更像 Cargo 给项目留下的一份依赖解析结果。
通常也不应该直接手动编辑它。官方文档明确建议由 Cargo 维护这个文件。
到这里,我才开始理解 Cargo
如果把这一章压缩成几张图,其实没有那么复杂。
最开始:
main.rs ↓ rustc ↓ executable
这是:“我有一段 Rust 代码,怎么把它编译出来?”
但项目稍微认真一点以后:
Rust project
├── Cargo.toml → 项目 + 依赖声明
├── Cargo.lock → 精确依赖解析结果
├── src/ → Rust source code
└── target/ → debug / release 这时候 Cargo 才真正开始有意义。
它帮我处理的已经不只是“运行一下 Rust”。
而是:
这也是为什么 Cargo 不只是一个“命令行工具”。
它实际上是 Rust 项目工作流的一部分。
官方对 Cargo 的定位也很明确:它是 Rust 的 package manager,负责下载依赖、编译 package、生成可分发 package,并与 Rust 生态的 package registry 配合。
但我现在还不想把 Cargo 学得太深
这里其实有一个很容易犯的错误。
看到 Cargo 以后,我完全可以继续往下钻:
workspace profiles features dependency resolver crates.io build scripts target specification Cargo configuration publishing
这些东西当然都重要。
但如果现在全部塞进来,我估计最后又会变成:
我看完了很多 Cargo 文档,但还是不知道一个 Rust 项目到底怎么跑起来。
所以这一篇我暂时只停在这里。
我现在只需要建立一个足够实用的模型:
Cargo
│
├── cargo new
│ 创建项目
│
├── cargo check
│ 快速检查代码
│
├── cargo build
│ 构建项目
│
├── cargo run
│ 构建并运行
│
└── cargo build --release
构建 release 版本 剩下的以后遇到再学。
我真正想留下来的,不是这几个命令
如果只是记命令,这一篇其实没什么必要。
这些命令过两周我可能就忘了。
真正让我觉得有变化的是,我开始慢慢区分两个层次:
这一章不是记住几个命令,
而是开始区分两个层次。
① Rust compiler
② Rust project
上一篇我还在想:Rust 代码到底怎么跑起来?这一篇开始,我的问题变成了: 一个 Rust 项目到底是怎么工作的? The important thing wasn't learning another command. It was understanding the system behind the command.
我觉得这两个问题还是不太一样的。
还有一个我现在特别想提醒自己的问题
我现在学 Rust,其实和以前学编程有一个挺大的不同:
我手边一直有 AI。
代码不会写?问 AI。
编译错误?贴给 AI。
不知道某个 crate 怎么用?问 AI。
甚至整个项目不知道怎么组织,也可以让 AI 给一个方案。
这当然非常方便。
但我越来越觉得,这里面有一个坑。
如果每次遇到错误,我都直接把 error[E....] 复制给 AI,然后把它给我的修改直接贴进去。
最后可能会出现一个挺奇怪的结果:
AI 越来越会 Rust 我会复制 Rust
所以我现在想给自己定一个很简单的规则。
先自己读错误。
先看 error[E...],再看 --> src/main.rs:xx,看看 compiler 到底在说什么。
而且问 AI 的时候,我希望以后更多问:
“这个错误是什么意思?为什么 Rust 会这样检查?”
而不是:
“帮我改掉。”
因为前一种是在学习。
后一种只是让程序继续跑。
这两件事情,看起来结果一样,其实完全不是一回事。
下一步
到这里,我其实还没真正开始学 Rust 最难的部分。
Ownership 还没真正进入。
Borrowing 还没开始折磨我。
Lifetime 更不用说。
但我觉得这一篇还是有必要。
因为至少现在,当我看到一个 Rust 项目时,不再只会盯着 src/main.rs。
我开始知道 Cargo.toml、Cargo.lock、src/、target/ 这些东西各自是在干什么。
而这可能才是真正进入一个 Rust 项目的第一步。
下一篇继续。
这次应该就不能只靠 println!("Hello, world!"); 混过去了。
参考资料
- [01] First Steps with Cargo — The Cargo Book
- [02] Why Cargo Exists — The Cargo Book
- [03] Appendix E: Editions — The Rust Programming Language
- [04] The Rust Programming Language
- [05] Hello, Cargo! — The Rust Programming Language
- [06] cargo check — The Cargo Book
- [07] Creating a New Package — The Cargo Book
- [08] Cargo.toml vs Cargo.lock — The Cargo Book
- [09] The Cargo Book