Rust Cargo 与 crate
在快速上手一页里,你已经用过 cargo new 和 cargo run。那只是个开始。Cargo 既是 Rust 的构建工具,也是它的包管理器。它负责编译代码、运行测试、格式化文件,还能下载别人写好的库。
这些库叫作 crate。你可以把 crate 想象成应用商店里的应用:别人已经解决了某个问题(读取 JSON、发送网络请求、解析日期),你只要一条命令就能把他们的方案加进自己的项目。这一页会告诉你具体怎么做,以及怎样挑选一个好的 crate。
crate 和包
Section titled “crate 和包”有两个词会经常出现:
- crate 是编译器构建的一个 Rust 代码单元。它要么是二进制 crate(可以运行的程序,从
src/main.rs开始),要么是库 crate(给其他程序用的代码,从src/lib.rs开始)。 - 包(package)就是
cargo new创建出来的东西:一个文件夹,里面有一个Cargo.toml文件和一个或多个 crate。
日常交流中,大家两种情况都说“crate”。当有人说“用 serde 这个 crate”时,意思是“把 crates.io 上的 serde 包加进你的项目”。
Cargo.toml 里有什么
Section titled “Cargo.toml 里有什么”每个包都有一个 Cargo.toml 文件。它用 TOML(英文)写成,这是一种简单的配置格式。下面是 cargo new hello_rust 生成的内容:
[package]name = "hello_rust"version = "0.1.0"edition = "2024"
[dependencies][package]描述你的项目。如果你把它发布出去,别人看到的就是name和version。edition选择使用哪个版次(edition)的 Rust 语言。版次(2018、2021、2024)让 Rust 可以不断改进,又不会破坏旧代码。保留cargo new写好的值就行。[dependencies]列出你的代码用到的 crate。一开始是空的。
随着项目变大,你可能还会看到更多的段落:
[dependencies]serde = { version = "1.0", features = ["derive"] }serde_json = "1.0"
[dev-dependencies]# 只在测试和示例中使用,不会进入你真正的程序pretty_assertions = "1.4"
[profile.release]# `cargo build --release` 的设置opt-level = 3用 cargo add 添加依赖
Section titled “用 cargo add 添加依赖”你可以手动编辑 [dependencies],但最简单的办法是用 cargo add。它会在 crates.io 上找到最新版本,并替你写好那一行:
cargo add serde_json有些 crate 带有可选的部分,叫作 feature(特性)。为了让构建保持精简,它们默认是关闭的。用 --features 来打开:
cargo add serde --features derive执行完这两条命令后,你的 Cargo.toml 里会出现类似这样的内容:
[dependencies]serde = { version = "1.0.228", features = ["derive"] }serde_json = "1.0.145"现在就可以在代码里使用这个 crate 了。下一次 cargo run 会下载它、编译一次,再链接进你的程序:
use serde_json::json;
fn main() { let crab = json!({ "legs": 10, "name": "Ferris" });
println!("{}", crab); println!("名字:{}", crab["name"]);}输出:
{"legs":10,"name":"Ferris"}名字:"Ferris""Ferris" 两边带着引号,是因为 crab["name"] 仍然是一个 JSON 值,而不是普通的 Rust 字符串。
Rust Playground 已经内置了最流行的那些 crate,所以这段代码不用 cargo add 就能在那里运行。在你自己的电脑上,需要先执行 cargo add serde_json。关于这个 crate 的更多内容,可以看用 Serde 处理 JSON 一页。
不再需要某个 crate 时,运行 cargo remove serde_json 就能把它删掉。
版本要求和 Cargo.lock
Section titled “版本要求和 Cargo.lock”写 serde_json = "1.0" 时,你要的并不是正好 1.0 这个版本,而是“任何与 1.0 兼容的版本”。Cargo 遵循语义化版本(SemVer):版本号的格式是 MAJOR.MINOR.PATCH(主版本号.次版本号.修订号),只有主版本号的变化才允许破坏你的代码。
| 你写的 | Cargo 可能使用的版本 | 原因 |
|---|---|---|
"1.0"(等同于 "^1.0") |
>=1.0.0 且 <2.0.0 |
所有 1.x 版本都应该互相兼容 |
"1.4.2" |
>=1.4.2 且 <2.0.0 |
至少 1.4.2,但仍然是 1.x |
"0.4" |
>=0.4.0 且 <0.5.0 |
1.0 之前,次版本号的变化就算破坏性变更 |
"~1.4" |
>=1.4.0 且 <1.5.0 |
只接受修订号的更新 |
"=1.4.2" |
正好是 1.4.2 |
很少需要 |
那么具体的版本由谁决定呢?这就是 Cargo.lock 的工作。第一次构建时,Cargo 会挑出符合你要求的最新版本,并把它们记在 Cargo.lock 里。之后的每次构建都使用完全相同的版本,无论是在你的电脑上还是在别人的电脑上。
- 不要手动编辑
Cargo.lock。它由 Cargo 管理。 - 把它提交到 git,尤其是程序项目。这样你的队友和 CI 构建出来的东西就和你的完全一样。
cargo update会升级到仍然符合Cargo.toml的最新版本,并重写Cargo.lock。它永远不会跳到新的主版本;要升级主版本,得你自己去改Cargo.toml。
挑选好用的 crate
Section titled “挑选好用的 crate”有三个网站你会反复用到:
- crates.io 是官方的 crate 仓库,所有公开的 crate 都在这里。可以搜索“csv”或“date”这样的词。
- docs.rs 托管着每个 crate 的文档,都是自动构建的。比如 docs.rs/serde_json。
- lib.rs 是一个非官方的、速度更快的索引,按类别给 crate 分组,很适合随便逛逛。
解决同一个问题的 crate 往往有好几个,该怎么选呢?添加之前,花一分钟看看这几点:
- 下载量。 流行的 crate 有几百万次下载。这意味着在你之前,已经有很多人发现并修复了其中的 bug。
- 最近的更新。 看看最新一次发布的日期。好几年没变过的 crate 也许依然没问题(有些 crate 就是已经“完工”了),但要确认它的代码仓库里没有堆满无人回应的 issue。
- 文档。 打开它在 docs.rs 上的页面。好的 crate 首页清晰,还有能直接复制的示例。
- 版本号在 1.0 或以上。 这不是必须的,但它说明作者承诺 API 是稳定的。
- 依赖少。 你加进来的每个 crate 都会带来它自己的依赖。越小越容易让人放心。
拿不准的时候,就搜索“rust”加上你要解决的问题,看看社区反复推荐的是哪个 crate。
常用的 Cargo 命令
Section titled “常用的 Cargo 命令”这些命令不用死记硬背。把这张表放在手边,用着用着就记住了:
| 命令 | 作用 |
|---|---|
cargo new my_app |
创建一个新的二进制项目(加上 --lib 则创建库) |
cargo build |
编译到 target/debug/ |
cargo run |
编译并运行(参数写在 -- 后面:cargo run -- input.txt) |
cargo check |
检查错误,但不生成程序。比 build 快得多 |
cargo test |
运行测试(见测试) |
cargo fmt |
把所有代码格式化成标准风格 |
cargo clippy |
运行代码检查工具(linter),它会建议更简洁、更地道的写法 |
cargo doc --open |
为你的项目以及每个依赖生成 HTML 文档,然后打开它们 |
cargo update |
把 Cargo.lock 更新到最新的兼容版本 |
cargo build --release |
把优化过的程序编译到 target/release/ |
一个好习惯:写代码时经常运行 cargo check,提交前运行 cargo fmt 和 cargo clippy。cargo doc --open 也是个容易被忽略的宝藏,它给出的是你实际使用的那些 crate 版本的文档,而且离线也能用。
调试构建和发布构建
Section titled “调试构建和发布构建”默认情况下,Cargo 生成的是调试(debug)构建。它编译得快,还保留了额外的安全检查,比如整数溢出时会 panic。开发阶段用它再合适不过。
发布(release)构建(cargo build --release 或 cargo run --release)编译得慢一些,但运行起来快得多,常常能快 10 倍甚至更多。发布程序或者测量速度时用它。
你的代码甚至能知道自己是哪种构建:
fn main() { if cfg!(debug_assertions) { println!("调试构建:额外检查已开启"); } else { println!("发布构建:已针对速度优化"); }}输出:
调试构建:额外检查已开启在 Playground 里,可以在菜单中把“Debug”切换成“Release”,再运行一次,就能看到另一行输出。
同时包含二进制 crate 和库 crate
Section titled “同时包含二进制 crate 和库 crate”一个包可以同时包含一个库和一个二进制程序。这是一种很常见的布局:真正的逻辑放在 src/lib.rs 里,src/main.rs 保持精简,只负责调用它:
greeter/├── Cargo.toml└── src/ ├── lib.rs # 库 crate:可复用的逻辑 └── main.rs # 二进制 crate:你运行的程序pub fn greet(name: &str) -> String { format!("你好,{name}!")}fn main() { // 库的名字和包名相同 println!("{}", greeter::greet("Ferris"));}为什么要这样拆分?lib.rs 里的代码容易测试,其他程序(或者第二个二进制程序)也能复用它。Playground 只有一个文件,不过你可以用一个 pub mod 体会一下同样的感觉,就像模块一页里那样:
mod greeter { pub fn greet(name: &str) -> String { format!("你好,{name}!") }}
fn main() { println!("{}", greeter::greet("Ferris"));}输出:
你好,Ferris!初识工作空间
Section titled “初识工作空间”当一个项目发展成几个相互关联的包(比如一个命令行程序、一个 Web 服务器和一个共享库)时,可以把它们放进同一个工作空间(workspace)。它们共用一个 Cargo.lock 和一个 target/ 文件夹,所以所有东西一起构建,依赖版本也保持一致。
最顶层的 Cargo.toml 列出所有成员:
[workspace]members = ["app", "core"]resolver = "3"my_project/├── Cargo.toml # 上面那个工作空间文件├── Cargo.lock # 所有成员共用├── app/ # 一个二进制包└── core/ # 一个库包之后在顶层运行 cargo build 就会构建所有成员,cargo run -p app 则只运行其中一个。刚开始做项目时用不到这个,但在读别人的代码时,能认出它会很有帮助。
没添加 crate 就使用它。 如果你写了 use serde_json::json;,却忘了执行 cargo add serde_json,编译器就会找不到它:
error[E0432]: unresolved import `serde_json` --> src/main.rs:1:5 |1 | use serde_json::json; | ^^^^^^^^^^ use of unresolved module or unlinked crate `serde_json` | = help: you might be missing a crate named `serde_json`编译器说找不到名叫 serde_json 的模块或 crate,并提示你可能少了这个 crate。解决办法是运行 cargo add serde_json。也要检查一下拼写:crate 名里可能用 - 或 _,而在代码里 - 总是会变成 _。
忘了打开 feature。 如果 #[derive(Serialize)] 报错说找不到这个 derive 宏,多半是你添加 serde 时没加 --features derive。再运行一次 cargo add serde --features derive 就行,它会更新已有的那一行。
用调试构建来测速度。 如果觉得程序慢,先试试 cargo run --release,再考虑改代码。
1. 在 Playground 里使用 crate
Section titled “1. 在 Playground 里使用 crate”用 serde_json 的 json! 宏描述一个 crate,包含三个键:downloads(一个数字)、name 和 version。先打印整个值,再只打印版本号。
提示:照着用 cargo add 添加依赖里 crab 那个例子的样子写。在你自己的电脑上,要先运行 cargo add serde_json;Playground 里已经有了。
查看答案
use serde_json::json;
fn main() { let krate = json!({ "downloads": 500, "name": "serde_json", "version": "1.0" });
println!("{}", krate); println!("版本:{}", krate["version"]);}输出:
{"downloads":500,"name":"serde_json","version":"1.0"}版本:"1.0"变量叫 krate,是因为 crate 是 Rust 的保留字。这种拼法在 Rust 代码里是个常见的玩笑。和前面一样,版本号打印出来带着引号,因为它仍然是一个 JSON 值;用 Serde 处理 JSON 一页会讲怎样取出纯文本。
2. 只在调试构建中显示的消息
Section titled “2. 只在调试构建中显示的消息”写一个函数 debug_log(msg: &str),打印 [debug] 加上消息内容,但只在调试构建中打印。在 main 里的一个小计算前后调用它,并正常打印计算结果。
提示:把 println! 包在 if cfg!(debug_assertions) { ... } 里,就像调试构建和发布构建里那样。
查看答案
fn debug_log(msg: &str) { if cfg!(debug_assertions) { println!("[debug] {msg}"); }}
fn main() { debug_log("开始计算"); let mut total = 0; for n in 1..=10 { total += n; } debug_log("计算完成");
println!("1 到 10 的和是 {total}");}输出:
[debug] 开始计算[debug] 计算完成1 到 10 的和是 55把 Playground 切换到“Release”(或者在本地用 cargo run --release)再运行一次:只会打印最后一行。在真正的项目里,日志一页介绍的 crate 能把这件事做得更好,还支持 debug 和 info 这样的日志级别。
- Cargo 负责构建、运行、测试和格式化你的代码,还会从 crates.io 下载 crate。
Cargo.toml写的是你想要什么;Cargo.lock记录你实际得到的确切版本。两个都要提交。cargo add name --features x添加一个依赖。"1.0"的意思是“任何兼容的 1.x 版本”。- 判断一个 crate 好不好,看下载量、最近的更新,以及 docs.rs 上的文档是否完善。
- 写代码时用
cargo check,提交前用cargo fmt和cargo clippy,追求速度时用--release。 - 把可复用的逻辑放进
src/lib.rs,让src/main.rs保持精简。
小测验
1. 在 Cargo.toml 里,serde_json = "1.0" 是什么意思?
2. 哪个文件记录了构建时用到的每个依赖的确切版本?
3. 你想尽快检查代码有没有错误,不需要生成程序。哪条命令最合适?
4. 怎样添加 serde 并打开它的 derive feature?
得分: 0 / 4
- The Cargo Book(英文):Cargo 官方的指南和参考,涵盖它能做的一切。
- Specifying Dependencies — The Cargo Book(英文):版本要求的所有写法,以及 git 依赖和路径依赖。
cargo add— The Cargo Book(英文):cargo add的全部选项,比如--dev、--features和--no-default-features。- 更多关于 Cargo 和 Crates.io 的内容 —《Rust 程序设计语言》:发布配置、发布你自己的 crate,以及用
cargo install安装工具。 - Cargo 工作空间 —《Rust 程序设计语言》:一步步搭建一个包含一个二进制 crate 和两个库的工作空间。