猜数字游戏:第一个完整的 Rust 程序 — 第 2 章拆解
猜数字游戏:第一个完整的 Rust 程序
这一章做什么: 带你从零写出一个能玩的猜数字游戏——程序想一个 1 到 100 的随机数, 你反复猜,它告诉你大了还是小了,猜中退出。 一路会碰上并解决五个真实问题:怎么读键盘输入、怎么生成随机数、类型对不上怎么编译错误、 怎么让用户猜多次、输错格式怎么不崩溃。读完这章,你提前见过全书一半的核心概念。
1. 顶层全景:一个程序的六次迭代
主线例子就是这个游戏的完整成长史。每一步对应后面某一章的正式内容:
迭代 1:读入一行输入,原样打印 → let / String / read_line / expect(第 3、9 章)
迭代 2:加 rand crate,生成随机数 → 外部依赖、版本号、Cargo.lock(第 14 章)
迭代 3:比较 guess 和 secret_number → 编译报错!类型不匹配(E0308)(第 3 章)
迭代 4:trim().parse() 把字符串变数字 → 类型标注、变量遮蔽(第 3 章)
迭代 5:loop 循环 + break 退出 → 控制流(第 3 章)
迭代 6:match 处理 parse 的 Err → 错误恢复,不再崩溃(第 6、9 章)
图说:六次改动,五次是为了处理"出错的可能"。这就是 Rust 编程的日常节奏。注意这张图里有两次"故意让它崩"和一次"从崩改成不崩"——这本书从第一个项目起就训练一件事: 错误不是例外路径,是主路径;Rust 强迫你在写代码时就把它们摆上台面(第 9 章专门展开)。
2. 迭代 1:读入并打印一行——六个新概念挤在一行里
第一步的程序体是:
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {guess}");
}逐行拆,每行后面都挂着后面某一章:
use std::io; 把输入输出库引进来。
标准库(标准库是语言自带的函数库,std 是它的名字)有一小撮最常用的东西是默认引进的,这一小撮叫 prelude——不在 prelude 里的,就得用 use 显式引进来1。
io 不在 prelude 里,所以要写这一行。
let mut guess = String::new(); 声明一个可变变量,绑定到一个新的空字符串。
三个零件:let 声明变量;mut 让它可变——Rust 的变量默认不可变,不加 mut 就改不了,这是第 3 章细讲的设计2。
String::new() 里那个 :: 是"这个类型的关联函数"的意思。
关联函数(associated function)是定义在某个类型上的函数,用 类型::函数名 调用。
String::new 就是"String 这个类型自带的造新值函数"3。很多类型都有 new,这是约定俗成的名字。它和你熟悉的"对象的方法"是近亲,第 5 章会把这层关系讲透。
io::stdin().read_line(&mut guess) 读一行键盘输入,追加进 guess。
这里 &mut guess 的 & 表示引用(reference):把数据递给别人用,但不复制一份。
引用默认也不可变,所以要 &mut——read_line 得往里写内容,只借不给改是不行的4。引用的完整规则是全书的重头(第 4 章),现在只需记住这个形状。
.expect("Failed to read line") 处理"可能失败"。
read_line 除了读输入,还返回一个 Result 值。Result 是一个枚举(enumeration,一种"取值只可能是若干种之一"的类型),它的两个可能取值(variant)是 Ok 和 Err:Ok 表示操作成功、里面装着结果;Err 表示失败、里面装着失败原因5。
expect 是 Result 的方法:拿到 Err 就让程序崩溃并打印你给的这句话;拿到 Ok 就把里面装的值交给你6。
这里有一个不点破就会困惑的设计:不调用 expect,程序照样能编译,但你会收到警告——编译器警告你"这个 Result 没人理,它可能是个 Err"7。原书特意说:压掉这个警告的正确姿势不是忽略它,而是真的写错误处理8。这是 Rust 风格的第一次亮相:编译器盯着你对每个"可能失败"表态。
println!("You guessed: {guess}") 里的 {} 是占位符。
变量名可以塞进花括号;表达式值也行:println!("x = {x} and y + 2 = {}", y + 2) 会打印 x = 5 and y + 2 = 129。
3. 迭代 2:外部 crate 与版本号
Rust 标准库不带随机数。社区在 crates.io(Rust 生态发布开源项目的仓库网站)上维护了一个 rand crate10。用它分两步:
第 ① 步:在 Cargo.toml 里声明依赖。
[dependencies]
rand = "0.8.5"第 ② 步:cargo build。
输出很能说明 Cargo 的价值——你只写了 rand 一个名字,它却拉进来 15 个包:
$ cargo build
Updating crates.io index
Locking 15 packages to latest Rust 1.85.0 compatible versions
Adding rand v0.8.5 (available: v0.9.0)
Compiling proc-macro2 v1.0.93
Compiling libc v0.2.170
…(中间 11 个都是 rand 的依赖的依赖)…
Compiling rand v0.8.5
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.48s"0.8.5" 是语义化版本号(semver,一套版本号写法标准),写它的真实意思是 ^0.8.5:任何不低于 0.8.5、又低于 0.9.0 的版本都行——Cargo 认为 0.8.x 系列内部的公开接口是互相兼容的,所以会帮你挑这个范围内最新的补丁版11。
Cargo.lock 保证重现构建(reproducible build)。
第一次 build 时,Cargo 把算出来的确切版本写进 Cargo.lock;以后再 build,它直接照锁文件来,不再重新挑12。书里的例子:下周 rand 出了 0.8.6——哪怕它有 bug,你的项目也停在 0.8.5,直到你主动 cargo update(它只会在 0.8.x 范围内往上挑,不会跳到 0.999.0)13。正因为这个性质,Cargo.lock 通常和代码一起提交进版本库。
随机数生成本身两行:use rand::Rng; 把 Rng 这个 trait 引进作用域——trait 定义了一组方法,随机数发生器都实现它;不引进来,它的方法就不能用14。然后:
let secret_number = rand::thread_rng().gen_range(1..=100);1..=100 是区间表达式,两端都含(..= 表示包含右端),所以生成的是 1 到 100 的整数15。
4. 迭代 3 与 4:一次类型错误,和它的解法
加上比较逻辑,代码不编译了:
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}match 是 Rust 的多路分支:把值依次对每条"臂"(arm)的模式比对,命中哪条走哪条。Ordering 是标准库的枚举,三个值:Less / Greater / Equal——正好是比较的全部三种结果16。
但编译器报错:
error[E0308]: mismatched types
--> src/main.rs:22:21
|
22 | match guess.cmp(&secret_number) {
| --- ^^^^^^^^^^^^^^^^ expected `&String`, found `&{integer}`为什么错:guess 是 String(文本),secret_number 是数字。字符串和数字没法比大小。
Rust 有强静态类型系统(编译期确定每个值的类型,对不上就拒编),但也有类型推断——let mut guess = String::new() 时它自己推出 guess 是 String,不用你写;而 secret_number 你只给了 1..=100,它在几种数字类型里默认挑 i3217。
解法是转换,而不是抱怨:
let guess: u32 = guess.trim().parse().expect("Please type a number!");三个动作:trim() 掐掉用户按回车带进来的换行符(输入 5 再回车,实际进字符串的是 5\n 或 Windows 下的 5\r\n);parse() 把字符串解析成数字;: u32 是类型标注,显式告诉 Rust 要哪种数字(u32:无符号 32 位整数)18。
注意第一行的 let guess ——名字已经用过了。Rust 允许遮蔽(shadowing):用一个同名的新变量盖掉旧的,常用于"把值从一种类型换成另一种类型"这类转换19。有了 u32 标注,编译器反推 secret_number 也该是 u32,两边类型对齐,比较成立。
parse 会失败(用户敲 A% 就没法变成数字),所以它同样返回 Result——这里仍用 expect,失败就崩。先把"会崩"接受下来,迭代 6 再收拾它。
5. 迭代 5 与 6:循环、退出、以及"把崩溃改成继续"
loop 是无线循环。 把"提示输入—读入—比较"整段包进去,用户就能猜多次20。赢了的那个分支里加 break,猜中就跳出循环、程序结束。
于是只剩最后一个瑕疵:输非数字时程序直接 panic 崩溃。最后一版把 expect 换成 match:
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};读法:parse 成功 → 值是 Ok(数字),匹配第一条臂,把数字取出来;失败 → 值是 Err(错误信息),匹配第二条臂。Err(_) 里的下划线是通配符:不管什么错误都算数;那条臂的执行体 continue 表示"跳过本轮,回循环开头再猜"21。
$ cargo run
Guess the number!
Please input your guess.
foo ← 输入非数字
Please input your guess. ← 没崩,继续
You guessed: 61
You win!从 expect(崩)到 match(继续),这一改是全章的落点:错误处理从"祈祷别出错"变成"把每条路都写出来"。
6. 作者的判断与证据
这一章是教学项目章,判断成分少,但有两处可以标:
- "压掉警告的正确方式是真正写错误处理"——这是 Rust 社区的价值主张,用编译器警告机制作为证据呈现8。
- 版本号写成
0.8.5而不是=0.8.5,取舍是"自动拿到兼容范围内的补丁修复"换"偶尔踩到补丁回归"——书里没有展开这个权衡的另一面(0.8.6 也可能带回归,只能靠 lock 文件挡住),我们在第 5 节边界里补上。
判断(我们的,不是书里的): 这章的编排本身就是一种教学主张——先跑起来,再讲原理。 六个迭代里五个在处理错误和边界,等于在读者还没学理论之前就让他亲身体会 "Rust 把失败路径变成主路径"这门语言的性格。 如果错,会错在: 对完全没有编程经验的人,第一次编译就撞 E0308 类型错误可能劝退; 原书也意识到这点,在引言里建议 meticulous 型读者可以先跳过本章去读第 3 章。
7. 边界与局限
- 代码版本敏感:
rand = "0.8.5"、gen_range(1..=100)都是该书写就时的 API;rand 0.9 起接口有变化,照抄旧版号最稳。这也正好演示了 semver 的作用。 - "如何写好错误处理"这里只有 match/continue 一种形态;
?运算符、自定义错误类型等在第 9 章。 - trait 只在"use 引进来才能用方法"的意义上出现了一次,机制完全留给第 10 章。
8. 可带走的
- 变量默认不可变,要改就
let mut。 类型::new()是关联函数;名字!是宏;&mut x是可变引用。- 可能失败的操作返回
Result(Ok/Err);不理它,编译器警告;expect是"失败就崩"的偷懒写法。 - 标准库没有的东西去 crates.io 找;
Cargo.toml写名字 = "版本",版本遵循 semver,"0.8.5"意为^0.8.5。 Cargo.lock锁确切版本、保证重现构建,要进版本库;cargo update只在兼容范围内升级。- 字符串转数字:
trim().parse(),记得类型标注(: u32);转换常用遮蔽复用变量名。 - 多路分支用
match,臂 = 模式 + 代码;_是通配符。 loop无限循环,break跳出,continue下一轮。
9. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| prelude 与 use | Chapter 2: Programming A Guessing Game | text/09-ch02-chapter-2-programming-a-guessing-game.txt:84(搜「called the prelude」) |
关联函数与 :: | 同上 | text/09-ch02-chapter-2-programming-a-guessing-game.txt:137(搜「An associated function is a function」) |
| Result 与 expect | 同上 | text/09-ch02-chapter-2-programming-a-guessing-game.txt:184(搜「an enumeration」) · text/09-ch02-chapter-2-programming-a-guessing-game.txt:194(搜「expect method」) |
| 未用 Result 的警告 | 同上 | text/09-ch02-chapter-2-programming-a-guessing-game.txt:206(搜「unused」) |
semver 与 ^0.8.5 | 同上 | text/09-ch02-chapter-2-programming-a-guessing-game.txt:291(搜「semantic version specifier」) |
| Cargo.lock 与重现构建 | 同上 | text/09-ch02-chapter-2-programming-a-guessing-game.txt:356(搜「Reproducible Builds」) · text/09-ch02-chapter-2-programming-a-guessing-game.txt:382(搜「cargo update」) |
| rand 与 gen_range | 同上 | text/09-ch02-chapter-2-programming-a-guessing-game.txt:415(搜「gen_range」) |
| E0308 类型不匹配 | 同上 | text/09-ch02-chapter-2-programming-a-guessing-game.txt:536(搜「mismatched types」) |
| 遮蔽与 parse | 同上 | text/09-ch02-chapter-2-programming-a-guessing-game.txt:581(搜「helpfully Rust allows us to shadow」) |
| loop / break / match 处理输入 | 同上 | text/09-ch02-chapter-2-programming-a-guessing-game.txt:639(搜「infinite loop」) · text/09-ch02-chapter-2-programming-a-guessing-game.txt:740(搜「successfully turn the string into a number」) |
Footnotes
-
出处:「Chapter 2: Programming A Guessing Game」第 84 段(
text/09-ch02-chapter-2-programming-a-guessing-game.txt:84,搜「called the prelude」)。 ↩ -
出处:「Chapter 2: Programming A Guessing Game」第 118 段(
text/09-ch02-chapter-2-programming-a-guessing-game.txt:118,搜「immutable by default」)。 ↩ -
出处:「Chapter 2: Programming A Guessing Game」第 137 段(
text/09-ch02-chapter-2-programming-a-guessing-game.txt:137,搜「An associated function is a function」)。 ↩ -
出处:「Chapter 2: Programming A Guessing Game」第 162 段(
text/09-ch02-chapter-2-programming-a-guessing-game.txt:162,搜「this argument is a reference」)。 ↩ -
出处:「Chapter 2: Programming A Guessing Game」第 184 段(
text/09-ch02-chapter-2-programming-a-guessing-game.txt:184,搜「an enumeration」)与第 189 段(text/09-ch02-chapter-2-programming-a-guessing-game.txt:189,搜「Ok and Err」)。 ↩ -
出处:「Chapter 2: Programming A Guessing Game」第 194 段(
text/09-ch02-chapter-2-programming-a-guessing-game.txt:194,搜「expect method」)。 ↩ -
出处:「Chapter 2: Programming A Guessing Game」第 206 段(
text/09-ch02-chapter-2-programming-a-guessing-game.txt:206,搜「unused」)。警告全文:unusedResultthat must be used。 ↩ -
出处:「Chapter 2: Programming A Guessing Game」第 224 段(
text/09-ch02-chapter-2-programming-a-guessing-game.txt:224,搜「The right way to suppress the warning」)。 ↩ ↩2 -
出处:「Chapter 2: Programming A Guessing Game」第 247 段(
text/09-ch02-chapter-2-programming-a-guessing-game.txt:247,搜「y + 2 =」)。 ↩ -
出处:「Chapter 2: Programming A Guessing Game」第 268 段(
text/09-ch02-chapter-2-programming-a-guessing-game.txt:268,搜「yet include random number」)与第 332 段(text/09-ch02-chapter-2-programming-a-guessing-game.txt:332,搜「Crates.io is where」)。 ↩ -
出处:「Chapter 2: Programming A Guessing Game」第 291 段(
text/09-ch02-chapter-2-programming-a-guessing-game.txt:291,搜「semantic version specifier」)与第 293 段(text/09-ch02-chapter-2-programming-a-guessing-game.txt:293,搜「shorthand for」)。 ↩ -
出处:「Chapter 2: Programming A Guessing Game」第 357 段(
text/09-ch02-chapter-2-programming-a-guessing-game.txt:357,搜「rebuild the same artifact」)。 ↩ -
出处:「Chapter 2: Programming A Guessing Game」第 382 段(
text/09-ch02-chapter-2-programming-a-guessing-game.txt:382,搜「cargo update」)。 ↩ -
出处:「Chapter 2: Programming A Guessing Game」第 432 段(
text/09-ch02-chapter-2-programming-a-guessing-game.txt:432,搜「The Rng trait defines methods」)。 ↩ -
出处:「Chapter 2: Programming A Guessing Game」第 443 段(
text/09-ch02-chapter-2-programming-a-guessing-game.txt:443,搜「inclusive on the lower and upper bounds」)。 ↩ -
出处:「Chapter 2: Programming A Guessing Game」第 513 段(
text/09-ch02-chapter-2-programming-a-guessing-game.txt:513,搜「An arm consists of a pattern」)。 ↩ -
出处:「Chapter 2: Programming A Guessing Game」第 548 段(
text/09-ch02-chapter-2-programming-a-guessing-game.txt:548,搜「strong, static type system」)与第 554 段(text/09-ch02-chapter-2-programming-a-guessing-game.txt:554,搜「defaults to an i32」)。 ↩ -
出处:「Chapter 2: Programming A Guessing Game」第 593 段(
text/09-ch02-chapter-2-programming-a-guessing-game.txt:593,搜「newline character」)与第 601 段(text/09-ch02-chapter-2-programming-a-guessing-game.txt:601,搜「unsigned, 32-bit integer」)。 ↩ -
出处:「Chapter 2: Programming A Guessing Game」第 581 段(
text/09-ch02-chapter-2-programming-a-guessing-game.txt:581,搜「helpfully Rust allows us to shadow」)。 ↩ -
出处:「Chapter 2: Programming A Guessing Game」第 639 段(
text/09-ch02-chapter-2-programming-a-guessing-game.txt:639,搜「infinite loop」)。 ↩ -
出处:「Chapter 2: Programming A Guessing Game」第 748 段(
text/09-ch02-chapter-2-programming-a-guessing-game.txt:748,搜「catch-all value」)。 ↩