本篇目标:
不学任何新语法,只把「写代码 → 问编译器 → 看懂它说什么 → 改对」这条回路跑通。这条回路是 Rust 唯一真正免费的午餐,也是新手流失率最高的地方。
一、为什么第一课不讲语法
大多数 Rust 教程的第一课是「变量与可变性」,第二课是「数据类型」。这种安排的隐患在于:读者在前三页就会遇到 borrow checker 的报错,而那时他们还不知道如何阅读报错信息。于是编译器从「帮手」变成了「拦路虎」------很多人放弃 Rust,不是因为所有权难,而是因为看不懂错误提示。
Rust 编译器是市面上最愿意沟通的编译器之一:它会指出出错位置、说明期望与实际类型、给出修复建议,甚至提供错误码的详细解释。但这套机制需要你主动去读。因此,第一课的任务就是学会解读这些反馈。
二、先把环境搭到能跑
2.1 安装
Linux/macOS
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Windows:下载 rustup-init.exe 运行,选默认选项(MSVC)即可
装完后确认三件事:
rustc --version # 例如 rustc 1.89.0 (29483883e 2025-08-04)
cargo --version
rustup show # 看默认 toolchain 和 edition
两个必须养成的习惯:
- 锁版本。博文或教程里看到的代码,如果编译不过,第一件事是比对 rustc 版本和 edition。Rust 2024 edition 在 impl Trait 返回值捕获规则、let chains 上有变动,跨版本抄代码是最常见的无谓挫败。
- 本地离线文档:rustup component add rust-docs,然后 rustup doc 会在浏览器打开标准库文档。断网也能查,比网上二手文章可靠得多。
2.2 六个命令,各自干什么
命令 干什么 什么时候用
cargo new name 新建项目 一次
cargo check 只检查类型/借用,不生成二进制 最常用,写完一小段就敲一次
cargo build 真正编译出二进制 要跑的时候
cargo run build + 运行 要跑的时候
cargo test 跑测试 每写完一个函数就跑
cargo clippy lint,抓「能编译但写得烂」 每次提交前
cargo fmt 自动格式化 每次提交前
重点区分 check 和 build:check 跳过代码生成和链接,速度通常快几倍。初学者的典型节奏是:写两行 → cargo check → 修报错 → cargo check。不要把 cargo build 当反馈工具,那几十秒的等待会显著拖慢学习节奏。
顺便,clippy 的建议值得认真对待。初学者常觉得它啰嗦,但它的每一条 lint 背后都是真实踩过的坑。遇到不懂的警告,cargo clippy --explain <lint名>。
三、先给你一个报错,别急着划走
新建个项目,把下面这段粘进 src/main.rs,然后 cargo check:
rust
fn main() {
let s = String::from("hello");
let len = calc_len(s);
println!(""{}" 的长度是 {}", s, len); // ← 这行会红
}
fn calc_len(s: String) -> usize {
s.len()
}
你会看到类似这样的输出(精简版):
errorE0382: borrow of moved value: s
--> src/main.rs:4:33
|
2 | let s = String::from("hello");
| - move occurs because s has type String, which does not implement the Copy trait
3 | let len = calc_len(s);
| - value moved here
4 | println!("...{}", s, len);
| ^ value borrowed here after move
|
help: consider cloning the value if the performance cost is acceptable
|
3 | let len = calc_len(s.clone());
| ++++++++
这段报错包含五个信息层,请逐层对应:
- 错误码 E0382 ------ 这是可检索的关键词。rustc --explain E0382 会给出完整说明与多种解法。养成查错误码的习惯,比死记硬背规则有效十倍。
- 主箭头 --> src/main.rs:4:33 ------ 指出报错位置,即编译器发现问题的地方。注意:这不一定是问题根源所在。
- 上下文链(那些 | 和 -):第 2 行标注 s 在此声明;第 3 行提示值在此被 move;第 4 行显示在此处又被借用。这三行连起来才是完整的叙事,单看第 4 行容易感到困惑。初学者常犯的错误是只盯着红色那一行。
- 原因说明:String 没有实现 Copy trait。这是核心知识点,后文会详细展开。
- help 建议:编译器建议加 .clone()。这里需要特别注意:help 建议不一定是最优解,它只是「能让代码编译通过」的解。本例中 .clone() 确实可行,但更优的做法是修改函数签名传引用(见练习三)。
再看一种常见形态------类型不匹配:
errorE0308: mismatched types
--> src/main.rs:7:24
|
7 | fn double(x: i32) -> i32 { x.to_string() }
| ------ ^^^ expected i32, found String
| implicitly returns () as its body has no tail or return expression
注意 expected ..., found ... 这个句式,它会伴随整个 Rust 生涯。左边是承诺(签名声明的),右边是实际(你给的)。 以后看到任何 E0308,先在脑子里翻译成「承诺 vs 实际」,一半的问题当场就能定位。
四、几个必须提前知道的「坑型」
坑 1:第一个报错往往掩盖了后面的。
一次报 20 个错时,只修第一个,然后重新 cargo check。后续很多报错可能是前一个错误引发的连锁反应,修好源头后会自行消失。一次性全改反而容易改偏。
坑 2:不要盲目加 .clone()、unwrap() 或 unsafe 来消掉报错。
这三个操作都能让编译器闭嘴,但代价各不相同:.clone() 带来性能损耗,unwrap() 留下运行时 panic 隐患,unsafe 则直接放弃了编译器的保护。它们可作为临时手段,但不宜成为最终答案。后面课程会逐一讲解替代方案。
坑 3:报错指向的位置 ≠ 错误根源。
特别是生命周期和泛型推导失败时,编译器往往在使用处报错,而问题出在定义处的签名上。遇到「明明看着没错却报错了」的情况,优先检查函数签名和类型注解。
坑 4:cargo clean 不是万能药。
很多人一编译不过就先 cargo clean。Rust 的增量编译很可靠,清缓存能解决的问题极少。真遇到「改对了还报错」,先试试关掉 IDE 的 language server 重启,或者检查是不是有两个版本的依赖冲突。
坑 5:IDE 的红波浪线滞后于 cargo check。
以终端里 cargo check 的输出为准。分析器偶尔会卡住或报假错,但编译器不会说谎。
五、练习
所有练习都在同一个 cargo 项目里做即可(cargo new lesson1)。每题都故意给了一段不能编译或写得糟糕的代码,你的任务是修好它。每题配了测试桩,cargo test 全绿就算过。
建议先自己做,再看后面的参考思路。
练习 1(填空,热身)
补全函数签名,让测试通过:
rust
// TODO: 补全签名
fn add(a: ___, b: ___) -> ___ {
a + b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn t() { assert_eq!(add(3, 5), 8); }
}
参考思路
i32 最直接。也可以写泛型 impl Add<Output = T>,但那属于超纲,本课不要求。本课答案:fn add(a: i32, b: i32) -> i32。
练习 2(改错,读报错)
下面这段会报 两个 错。请先 cargo check,读懂每一条,再动手改:
rust
fn greet(name: String) -> String {
format!("Hello, {}!", name)
}
fn main() {
let who = String::from("Rust");
let msg = greet(who);
println!("{}", msg);
println!("再见, {}", who); // ← 目标:让这行也能编译
}
要求:不许用 .clone()(先试着自己撞墙,再往下看)。
参考思路
两种解法。妥协版:greet(who.clone())------能过,但多一次分配。优雅版:改签名 fn greet(name: &str) -> String,调用处 greet(&who)。后者是 Rust 的惯用写法:读数据就用借用,别抢所有权。记住这条,后面 .clone() 会少一大半。
练习 3(改错,expected/found)
rust
fn double(x: i32) -> i32 {
x * 2; // ← 注意这个分号
}
fn main() {
let n: i32 = "42"; // ← 还有这个
println!("{}", double(n));
}
两个错分别是什么错误码?expected/found 各是什么?修好后思考:那个分号为什么会导致 expected i32, found ()?(() 是单元类型,即「没有值」。这个坑你会反复踩。)
练习 4(反模式:满屏 unwrap)
rust
fn parse_pair(s: &str) -> (i32, i32) {
let parts: Vec<&str> = s.split(',').collect();
let a = parts[0].parse::<i32>().unwrap();
let b = parts[1].parse::<i32>().unwrap();
(a, b)
}
这段能编译、能跑,但输入 "1,x" 时会 panic。重构它:返回 Result<(i32, i32), String>,用 ? 或 map_err 处理错误,一个 unwrap() 都不许留。测试桩请自己写三条:正常输入、非数字、缺第二部分。
参考思路
rust
fn parse_pair(s: &str) -> Result<(i32, i32), String> {
let mut parts = s.split(',');
let a = parts.next().ok_or("缺少第一个数")?.parse::<i32>()
.map_err(|_| "第一个数不是整数")?;
let b = parts.next().ok_or("缺少第二个数")?.parse::<i32>()
.map_err(|_| "第二个数不是整数")?;
Ok((a, b))
}
关键点:partsn 越界会 panic,改用迭代器 + next() + ok_or 才能把「缺元素」变成可控错误。这是 unwrap 最常见的藏身之处。
练习 5(clippy 专场)
下面这段能编译能跑,但 cargo clippy 会给出至少三条建议。全部修掉,并说出每条 lint 的名字:
rust
pub fn sum(v: &Vec<i32>) -> i32 {
let mut result = 0;
for i in 0..v.len() {
result = result + v[i];
}
return result;
}
参考思路
典型建议:&Vec → &i32(ptr_arg)、for i in 0...v.len() → for x in v(needless_range_loop)、result = result + vi → result += vi 或直接 v.iter().sum()(iter_cloned_collect / 风格建议)、末尾 return result; → result(needless_return)。
其中 &Vec → &T 这一条值得背下来:函数参数永远用切片而非 Vec 引用,因为切片更通用(数组、Vec、其他切片都能传)。
练习 6(开放题,收尾)
写一个 CLI 小工具 wordcount:从 stdin 或文件读入文本,输出行数、单词数、字符数(模仿 wc)。要求:
- 用 clap 或手动解析 std::env::args() 都行(第一课不必纠结)
- 文件不存在时不能 panic,要打印友好错误并返回非零退出码(提示:std::process::exit,或让 main 返回 Result<(), Box>)
- 至少两个 #test
- 跑过 cargo clippy 零警告、cargo fmt
这是整个系列的种子项目。后面学到模块、错误类型、迭代器时,我们会反复回来重构它。请保留这个仓库。
六、本课可以跳过的部分
- rustup target add、交叉编译、自定义 toolchain ------ 以后做发布才需要。
- Cargo.toml 里的 profile.release、features、workspace ------ 第四课之后再说。
- unsafe、裸指针、Pin、async ------ 一个字都别看。
- 各种 IDE 插件的深度配置 ------ 能用就行,别在这上面耗一下午。
七、延伸阅读(按优先级)
- rustc --explain ------ 官方错误索引,最权威。
- 《The Rust Programming Language》第 1 章(安装)+ 附录「常用术语」。
- Rust By Example ------ 想看语法示例时查它,别当主线。
- Clippy lint 列表 ------ 遇到不认识的警告去查。
避坑提醒:
网上大量「2026 Rust 入门最佳实践」「Rust 学习路线」类文章是 AI 批量生成的,里面引用的案例和版本号经常对不上。一切以 rust-lang.org、官方 book、crates.io 文档为准。包括我这篇里的版本号------动手前请用 rustc --version 核对。
八、下节课预告
第二课讲所有权:移动、借用、Copy、切片。我们会用同一个技巧------先给你一段逻辑正确但编译不过的代码,让你亲自撞上借用检查器,再解释规则。如果你能把本课练习 2 和练习 3 的报错逐句讲给自己听,下一课的门槛就已经低了一半。