Skip to content

练习与自测

本章练习共 7 题,答案折叠在每题下方。建议先自己写、编译通过后再展开答案;难度标记:★☆☆ 基础 / ★★☆ 综合 / ★★★ 挑战。

练习 1:环境自检与工具链信息

难度:★☆☆

要求:在本机完成环境自检,并用一条命令输出下面四项信息(可以多条命令组合): rustc 版本;cargo 版本;当前默认工具链名称与宿主平台;rustfmtclippy 是否已安装。 把输出贴出来,并说明 rustup showactive 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 给出的「为什么是这个工具链」,优先级从高到低大致是:

  1. rustup run <toolchain> ... 的一次性覆盖;
  2. 环境变量 RUSTUP_TOOLCHAIN
  3. 当前目录或其祖先目录里的 rust-toolchain.toml / rust-toolchain 文件(目录覆盖);
  4. rustup override set <toolchain> 设置的目录级覆盖;
  5. rustup default 设置的全局默认值(默认安装就是这一条)。

所以「明明装了 nightly,构建却还在用 stable」通常不是装错了,而是优先级更高的一层在起作用—— 先用 rustup showactive 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 Name

Cargo.toml

toml
[package]
name = "hello_cli"
version = "0.1.0"
edition = "2024"

[dependencies]

src/main.rs

rust
fn main() {
    println!("Hello, world!");
}

.gitignore

txt
/target

target/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.2cargo 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 tempconv
toml
# 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 = true
toml
# 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 = true
rust
// 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

要点解析

  1. Cargo.toml不能有 [package],否则根也成了一个包,与 members 的关系会变复杂。
  2. resolver = "3" 是 edition 2024 对应的依赖解析器;写出来可以让「以旧 edition 写的根清单」 也获得 2024 的 feature 解析行为。
  3. [lints] workspace = true 是关键:workspace 级 lint 不会自动下发,必须显式继承。 设错会静默无效——验证办法是故意写一处 unsafe {},看 cargo check 是否报 forbid
  4. workspace 共用一份 Cargo.lock 与一个 target/,因此 -p 是日常指定成员的标准手段。

练习 5:添加依赖与 dbg! 的使用限制

难度:★★☆

要求:在一个空工程里执行 cargo add randcargo add tokio --features full, 然后:贴出生成的 [dependencies] 小节;在 src/main.rs 里用 rand 生成一个 1..=6 的随机数; 说明为什么不建议把 dbg! 留在提交的代码里,以及 [lints.clippy] 里哪个配置能兜住这件事。

提示rand 的常用 API 是 rand::random_rangerand::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 integer
rust
// 片段 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 时才会与其他借用冲突。 (这里还有一个前提:ab 都是共享引用,且都指向同一块只读使用,因此不违反别名规则。)

要点解析:这两个片段放在一起,是为了破除两个新手错觉: 「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 消息都走 stderrprintln! 走 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_BACKTRACE

dbg!println!("{:?}", x) 的分工:

维度dbg!(x)println!("{:?}", x)
输出内容自动附带文件、行号、列号与表达式源码只有你自己写的内容
返回值返回 x 本身,可内联进表达式(let b = dbg!(a) + 1;返回 ()
目标流stderrstdout
接收类型可一次传多个参数(dbg!(a, b)需自己格式化多个值
适用场景临时定位「这个值在这里到底是多少」正式的、稳定的程序输出

要点解析dbg! 能内联是它最大的便利——不必为了打印而把表达式拆成临时变量。但正因为返回原值, 它也能被塞进 if dbg!(cond) { ... } 这类位置,很容易被忘记删除,所以一定要配 dbg_macro = "warn"。 另外,养成习惯:索引越界前先想清楚要不要用 v.get(idx)——它返回 Option<&T>,把「越界」 变成必须处理的分支,而不是让程序崩掉(错误处理见〈结构体与 trait〉)。

本章小结 / 自测清单


内容以 rustc 1.98.1 · Rust 2024 edition 为基准