跳转到内容

Rust Cargo 与 crate

在快速上手一页里,你已经用过 cargo new 和 cargo run。那只是个开始。Cargo 既是 Rust 的构建工具,也是它的包管理器。它负责编译代码、运行测试、格式化文件,还能下载别人写好的库。

这些库叫作 crate。你可以把 crate 想象成应用商店里的应用:别人已经解决了某个问题(读取 JSON、发送网络请求、解析日期),你只要一条命令就能把他们的方案加进自己的项目。这一页会告诉你具体怎么做,以及怎样挑选一个好的 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 文件。它用 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

你可以手动编辑 [dependencies],但最简单的办法是用 cargo add。它会在 crates.io 上找到最新版本,并替你写好那一行:

Terminal window
cargo add serde_json

有些 crate 带有可选的部分,叫作 feature(特性)。为了让构建保持精简,它们默认是关闭的。用 --features 来打开:

Terminal window
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 就能把它删掉。

写 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。

有三个网站你会反复用到:

  • crates.io 是官方的 crate 仓库,所有公开的 crate 都在这里。可以搜索“csv”或“date”这样的词。
  • docs.rs 托管着每个 crate 的文档,都是自动构建的。比如 docs.rs/serde_json。
  • lib.rs 是一个非官方的、速度更快的索引,按类别给 crate 分组,很适合随便逛逛。

解决同一个问题的 crate 往往有好几个,该怎么选呢?添加之前,花一分钟看看这几点:

  1. 下载量。 流行的 crate 有几百万次下载。这意味着在你之前,已经有很多人发现并修复了其中的 bug。
  2. 最近的更新。 看看最新一次发布的日期。好几年没变过的 crate 也许依然没问题(有些 crate 就是已经“完工”了),但要确认它的代码仓库里没有堆满无人回应的 issue。
  3. 文档。 打开它在 docs.rs 上的页面。好的 crate 首页清晰,还有能直接复制的示例。
  4. 版本号在 1.0 或以上。 这不是必须的,但它说明作者承诺 API 是稳定的。
  5. 依赖少。 你加进来的每个 crate 都会带来它自己的依赖。越小越容易让人放心。

拿不准的时候,就搜索“rust”加上你要解决的问题,看看社区反复推荐的是哪个 crate。

这些命令不用死记硬背。把这张表放在手边,用着用着就记住了:

命令 作用
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 版本的文档,而且离线也能用。

默认情况下,Cargo 生成的是调试(debug)构建。它编译得快,还保留了额外的安全检查,比如整数溢出时会 panic。开发阶段用它再合适不过。

发布(release)构建(cargo build --release 或 cargo run --release)编译得慢一些,但运行起来快得多,常常能快 10 倍甚至更多。发布程序或者测量速度时用它。

你的代码甚至能知道自己是哪种构建:

fn main() {
if cfg!(debug_assertions) {
println!("调试构建:额外检查已开启");
} else {
println!("发布构建:已针对速度优化");
}
}

输出:

调试构建:额外检查已开启
▶ 动手试一试

在 Playground 里,可以在菜单中把“Debug”切换成“Release”,再运行一次,就能看到另一行输出。

一个包可以同时包含一个库和一个二进制程序。这是一种很常见的布局:真正的逻辑放在 src/lib.rs 里,src/main.rs 保持精简,只负责调用它:

greeter/
├── Cargo.toml
└── src/
├── lib.rs # 库 crate:可复用的逻辑
└── main.rs # 二进制 crate:你运行的程序
src/lib.rs
pub fn greet(name: &str) -> String {
format!("你好,{name}!")
}
src/main.rs
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!
▶ 动手试一试

当一个项目发展成几个相互关联的包(比如一个命令行程序、一个 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,再考虑改代码。

用 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 一页会讲怎样取出纯文本。

写一个函数 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