练习与自测
本章练习共 7 题,答案折叠在每题下方。建议先自己写、编译通过后再展开答案;难度标记:★☆☆ 基础 / ★★☆ 综合 / ★★★ 挑战。
练习 1:环境自检与工具链信息
难度:★☆☆
要求:在本机完成环境自检,并用一条命令输出下面四项信息(可以多条命令组合): rustc 版本;cargo 版本;当前默认工具链名称与宿主平台;rustfmt 与 clippy 是否已安装。 把输出贴出来,并说明 rustup show 里 active because 一行的含义。
提示:rustup show 已经包含的大部分信息,重点是解释而不是拼命令。
参考答案(先自己写再看)
参考答案(先自己写再看)
powershell
# 一次性打印四项自检信息
rustc --version # rustc 1.98.1 (48a229cea 2026-09-01)
cargo --version # cargo 1.98.1 (797e8a9bc 2026-08-05)
rustup show # 默认宿主、已装工具链、当前生效工具链
rustup component list --installed # 应能看到 rustfmt、clippy(rust-analyzer 视安装方式而定)参考输出(rustup show 关键片段):
text
Default host: x86_64-pc-windows-msvc
installed toolchains
--------------------
stable-x86_64-pc-windows-msvc (active, default)
active toolchain
----------------
name: stable-x86_64-pc-windows-msvc
active because: it's the default toolchain要点解析:active because 是 rustup 给出的「为什么是这个工具链」,优先级从高到低大致是:
rustup run <toolchain> ...的一次性覆盖;- 环境变量
RUSTUP_TOOLCHAIN; - 当前目录或其祖先目录里的
rust-toolchain.toml/rust-toolchain文件(目录覆盖); rustup override set <toolchain>设置的目录级覆盖;rustup default设置的全局默认值(默认安装就是这一条)。
所以「明明装了 nightly,构建却还在用 stable」通常不是装错了,而是优先级更高的一层在起作用—— 先用 rustup show 看 active because 那一行,再决定该清哪一层: rustup override unset(清目录覆盖)或删除项目里的 rust-toolchain.toml。
练习 2:创建工程并认识构建产物
难度:★★☆
要求:用一条 cargo new 创建名为 hello_cli 的二进制工程,然后: 打印 Cargo.toml 内容;打印 src/main.rs 内容;说明 .gitignore 里写了什么、为什么; 列出 cargo build 之后 target/debug/ 里出现的文件,并说出每个文件的用途。
提示:cargo new 默认会初始化 Git 仓库,注意观察 .git 目录是否出现。
参考答案(先自己写再看)
参考答案(先自己写再看)
powershell
cargo new hello_cli --bin
Set-Location hello_cli
Get-Content Cargo.toml
Get-Content src\main.rs
Get-Content .gitignore
cargo build
Get-ChildItem target\debug -File | Select-Object NameCargo.toml:
toml
[package]
name = "hello_cli"
version = "0.1.0"
edition = "2024"
[dependencies]src/main.rs:
rust
fn main() {
println!("Hello, world!");
}.gitignore:
txt
/targettarget/debug/ 里与本工程同名的主要文件(典型输出):
text
hello_cli.exe # 链接完成的可执行文件(cargo run 实际运行的就是它)
hello_cli.pdb # Program Database,MSVC 的调试符号文件,调试器用它对应源码行
hello_cli.d # 记录该目标的依赖文件清单,供增量编译判断是否需要重建
deps/ # 依赖 crate 与本 crate 的中间产物(.rlib/.rmeta)与旧版二进制
incremental/ # 增量编译缓存,dev profile 默认开启
build/ # 各 build.rs 的输出目录
.fingerprint/ # 指纹缓存:源码/参数/flags 的哈希,决定「谁需要重编」要点解析:.gitignore 只写 /target 的原因是——target/ 完全可由源码重建,体积以 GB 计; 而 Cargo.lock(首次构建后出现)应当提交,因为它记录依赖的精确版本。 注意 /target 只忽略项目根下的 target,workspace 里各成员不会各生成一份。
练习 3:读懂 Cargo.toml 与版本约束
难度:★★☆
要求:解释下面这段 Cargo.toml 中每个字段的含义,并回答两个问题: cargo add rand 之后如果 crates.io 上 rand 的最新版是 0.9.2,写进清单的版本约束是什么? 该约束允许 cargo 把 rand 升到 0.10.0 吗?
toml
[package]
name = "my_app"
version = "0.1.0"
edition = "2024"
rust-version = "1.85"
[dependencies]
rand = "0.9.2"
[dev-dependencies]
pretty_assertions = "1"提示:重点解释 rust-version 与依赖版本的 caret 语义,以及 0.x 版本的特殊规则。
参考答案(先自己写再看)
参考答案(先自己写再看)
逐字段说明:
| 字段 | 含义 |
|---|---|
name = "my_app" | 包名,同时是可执行文件名与 use my_app::... 的 crate 名(连字符会转为下划线) |
version = "0.1.0" | 本包版本,遵循 SemVer;发布到 crates.io 后同一版本号不可复用 |
edition = "2024" | 语言版次,决定语法与解析器行为,需要 rustc 1.85+ |
rust-version = "1.85" | MSRV:声明本包至少需要 rustc 1.85;cargo 在新旧混合依赖里会据此给出可读错误 |
[dependencies] rand = "0.9.2" | caret 语义,实际范围 >=0.9.2, <0.10.0 |
[dev-dependencies] | 只在 cargo test / bench / example 时编译,不进入最终产物 |
问题的答案:
- 若 crates.io 上
rand最新为0.9.2,cargo add rand会写入rand = "0.9.2"。 - 该约束不允许升到
0.10.0。因为主版本号为 0 时,SemVer 规定 minor 位承担了「破坏性变更」的职责, 所以 Cargo 对0.x.y采用>=0.9.2, <0.10.0。想放宽必须显式写rand = "0.9"(仍是<0.10), 或干脆rand = "0.10"并自己完成迁移。
要点解析:这张表里最容易被忽略的是 rust-version。它的作用是在解析依赖阶段就报错, 而不是等到编译时冒出「unresolved import」,属于对使用者的礼貌。日常经验是:库作者应把 MSRV 设成 「自己实际测试过的旧版本」,而不是随手写个最新版。
练习 4:搭建最小 workspace
难度:★★★
要求:搭一个最小 workspace,结构如下,并让它能跑起来:
text
tempconv/
├── Cargo.toml # 只含 [workspace]
└── crates/
├── core/ # 库:提供 celsius_to_fahrenheit
│ ├── Cargo.toml
│ └── src/lib.rs
└── cli/ # 二进制:调用 core 并打印结果
├── Cargo.toml
└── src/main.rs约束:根清单用 members 声明两个成员;cli 用路径依赖引用 core; 根清单开 [lints.rust] unsafe_code = "forbid",成员用 [lints] workspace = true 继承; 用 cargo run -p tempconv-cli 输出 100°C = 212°F。
提示:包名用连字符(tempconv-core),代码里 use 时要写成下划线(tempconv_core)。
参考答案(先自己写再看)
参考答案(先自己写再看)
目录与文件:
powershell
# 在练习目录执行(练习工程与你的其他项目分开存放)
mkdir tempconv\crates\core\src -Force
mkdir tempconv\crates\cli\src -Force
Set-Location tempconvtoml
# Cargo.toml(根):只有 [workspace],它本身不是一个包
[workspace]
resolver = "3"
members = ["crates/core", "crates/cli"]
[lints.rust]
unsafe_code = "forbid"toml
# crates/core/Cargo.toml
[package]
name = "tempconv-core"
version = "0.1.0"
edition = "2024"
[dependencies]
[lints]
workspace = truetoml
# crates/cli/Cargo.toml
[package]
name = "tempconv-cli"
version = "0.1.0"
edition = "2024"
[dependencies]
# 路径依赖:指向 workspace 内的兄弟 crate,改 core 的代码后 cli 立即用新版本
tempconv-core = { path = "../core" }
[lints]
workspace = truerust
// crates/core/src/lib.rs:库入口,公开 API 供 cli 与集成测试使用
/// 把摄氏度换算为华氏度。
pub fn celsius_to_fahrenheit(c: f64) -> f64 {
c * 9.0 / 5.0 + 32.0
}rust
// crates/cli/src/main.rs:只做 I/O,逻辑全在库里
fn main() {
// 包名带连字符时,导入要用下划线形式:tempconv-core -> tempconv_core
let f = tempconv_core::celsius_to_fahrenheit(100.0);
println!("100°C = {f}°F");
}运行:
powershell
# 在 workspace 根目录
cargo run -p tempconv-cli
cargo check --workspace --all-targets # 一次检查所有成员
cargo tree -p tempconv-cli # 确认 cli 依赖了 core输出:
100°C = 212°F
要点解析:
- 根
Cargo.toml里不能有[package],否则根也成了一个包,与 members 的关系会变复杂。 resolver = "3"是 edition 2024 对应的依赖解析器;写出来可以让「以旧 edition 写的根清单」 也获得 2024 的 feature 解析行为。[lints] workspace = true是关键:workspace 级 lint 不会自动下发,必须显式继承。 设错会静默无效——验证办法是故意写一处unsafe {},看cargo check是否报forbid。- workspace 共用一份
Cargo.lock与一个target/,因此-p是日常指定成员的标准手段。
练习 5:添加依赖与 dbg! 的使用限制
难度:★★☆
要求:在一个空工程里执行 cargo add rand 与 cargo add tokio --features full, 然后:贴出生成的 [dependencies] 小节;在 src/main.rs 里用 rand 生成一个 1..=6 的随机数; 说明为什么不建议把 dbg! 留在提交的代码里,以及 [lints.clippy] 里哪个配置能兜住这件事。
提示:rand 的常用 API 是 rand::random_range 或 rand::rng()(版本不同 API 不同,先看 cargo doc -p rand --open)。
参考答案(先自己写再看)
参考答案(先自己写再看)
powershell
cargo new dice --bin
Set-Location dice
cargo add rand
cargo add tokio --features full
Get-Content Cargo.toml生成的 [dependencies] 小节(版本号以当时 crates.io 最新为准):
toml
[dependencies]
rand = "0.9.2"
tokio = { version = "1", features = ["full"] }rust
// src/main.rs:演示用 rand 生成 1..=6 的随机点数
fn main() {
// rand::rng() 拿到线程局部的随机数生成器;random_range 的区间是「左闭右闭」
let mut rng = rand::rng();
let roll: u8 = rng.random_range(1..=6);
println!("掷出了 {roll}");
}输出示例(每次不同):
掷出了 4
dbg! 为什么不建议留在提交的代码里:
- 它把文件名与行号打进输出,属于调试期噪音,会让日志格式不稳定;
- 它输出到 stderr,会污染把 stderr 当错误通道的下游工具(CI 日志分析、
2>重定向); - 它可能泄漏敏感值(令牌、密码、个人数据);
- 它参与编译但不产生副作用,容易被漏删,久而久之没人敢删。
兜底配置:在 [lints.clippy] 里写 dbg_macro = "warn"(配合 CI 的 -D warnings 直接变成错误), 同理还可以加 todo = "warn"、unimplemented = "warn"、unwrap_used = "warn"。
要点解析:rand 的 API 在 0.8 → 0.9 有过改动(thread_rng() 变成 rng(), gen_range 变成 random_range),所以升级 0.x crate 一定要看 changelog—— 这正是「依赖版本语义」里 caret 对 0.x 特殊处理的现实意义。遇到 API 不存在时,先 cargo doc -p rand --open 看本地版本文档,而不是照抄网上旧文章。
练习 6:读报错判断编译是否通过
难度:★★★
要求:读下面两段代码与报错,判断编译是否通过;不通过的请解释错误原因并给出两种修法。
rust
// 片段 A
fn main() {
let s: String = 42;
println!("{s}");
}text
error[E0308]: mismatched types
--> src\main.rs:2:21
|
2 | let s: String = 42;
| ------ ^^ expected `String`, found integerrust
// 片段 B
fn main() {
let s = String::from("hi");
let a = &s;
let b = &s;
println!("{a} {b}");
}提示:A 看类型;B 想清楚「多个不可变借用」是否被允许——别把借用规则记成「一个变量只能借一次」。
参考答案(先自己写再看)
参考答案(先自己写再看)
片段 A:编译不通过。
error[E0308]: mismatched types 的意思是「声明了 String,给的却是整数 42」。Rust 没有 「整数自动装箱成字符串」的隐式转换,42 的默认类型是 i32,与 String 之间不存在自动转换。 两种修法:
rust
// 修法一:显式转换(编译器 help 里给的就是这条)
fn main() {
let s: String = 42.to_string();
println!("{s}");
}rust
// 修法二:改用不涉及所有权的 &str 字面量;纯拼接场景用 format!
fn main() {
let s: String = format!("{}", 42);
println!("{s}");
}片段 B:编译通过。
rust
fn main() {
let s = String::from("hi");
let a = &s; // 不可变借用 #1
let b = &s; // 不可变借用 #2:允许
println!("{a} {b}");
}输出:
hi hi
借用规则的正确表述是「同一作用域内,要么有任意多个不可变借用,要么有唯一一个可变借用」。 多个 &T 共存是允许的,因为它们都只能读;只有需要 &mut T 时才会与其他借用冲突。 (这里还有一个前提:a、b 都是共享引用,且都指向同一块只读使用,因此不违反别名规则。)
要点解析:这两个片段放在一起,是为了破除两个新手错觉: 「Rust 会自动做类型转换」——不会,as / From / to_string 都得显式写; 「一个变量只能有一个借用」——不对,限制只针对可变借用与共享借用的共存。 先记住「读可以多人,写只能独写,读写不能同时」,〈变量与流程控制〉一章会把它形式化为借用检查器规则。
练习 8:输出流向、退出码与 dbg! 对比
难度:★★★
要求:给定下面这段程序,说明它会输出什么、退出码是多少、输出分别走 stdout 还是 stderr; 然后按需打开回溯,并说出 dbg! 与 println!("{:?}") 在用途上的区别。
rust
fn main() {
let v = vec![10, 20, 30];
let idx = 4;
dbg!(idx);
println!("{}", v[idx]);
}提示:先在 PowerShell 里用 $LASTEXITCODE 看退出码,再设置 $env:RUST_BACKTRACE 重跑。
参考答案(先自己写再看)
参考答案(先自己写再看)
rust
fn main() {
let v = vec![10, 20, 30];
let idx = 4;
dbg!(idx); // stderr:打印 [src/main.rs:3:5] idx = 4,并返回 4(这里未使用)
println!("{}", v[idx]); // 索引 4 越界,运行期 panic
}实际运行结果(典型输出):
text
[src/main.rs:3:5] idx = 4
thread 'main' (18944) panicked at src\main.rs:4:20:
index out of bounds: the len is 3 but the index is 4
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace- 输出流向:
dbg!与 panic 消息都走 stderr;println!走 stdout。本例因为 panic 发生在println!求值参数时,所以 stdout 上什么都没有。用cargo run 2>$null会把 panic 信息也吞掉。 - 退出码:panic 结束进程,退出码为 101(Rust 约定;
std::process::exit(0)之类显式退出除外)。
powershell
cargo run
Write-Output "退出码 = $LASTEXITCODE" # 输出:退出码 = 101
# 打开回溯后重跑,stderr 里会多出 stack backtrace 段
$env:RUST_BACKTRACE = 1
cargo run
Remove-Item Env:\RUST_BACKTRACEdbg! 与 println!("{:?}", x) 的分工:
| 维度 | dbg!(x) | println!("{:?}", x) |
|---|---|---|
| 输出内容 | 自动附带文件、行号、列号与表达式源码 | 只有你自己写的内容 |
| 返回值 | 返回 x 本身,可内联进表达式(let b = dbg!(a) + 1;) | 返回 () |
| 目标流 | stderr | stdout |
| 接收类型 | 可一次传多个参数(dbg!(a, b)) | 需自己格式化多个值 |
| 适用场景 | 临时定位「这个值在这里到底是多少」 | 正式的、稳定的程序输出 |
要点解析:dbg! 能内联是它最大的便利——不必为了打印而把表达式拆成临时变量。但正因为返回原值, 它也能被塞进 if dbg!(cond) { ... } 这类位置,很容易被忘记删除,所以一定要配 dbg_macro = "warn"。 另外,养成习惯:索引越界前先想清楚要不要用 v.get(idx)——它返回 Option<&T>,把「越界」 变成必须处理的分支,而不是让程序崩掉(错误处理见〈结构体与 trait〉)。