猜数游戏

猜数游戏
猜数游戏
猜数游戏

  1. 开始:程序启动。
  2. 生成随机数:程序在1到100之间生成一个随机整数,并将其存储。提示用户输入猜测:程序提示用户输入一个数字,作为对随机数的猜测。
  3. 判断猜测:
    • 猜对了:如果用户的猜测与随机数相等,程序输出祝贺信息并结束游戏。
    • 猜错了:如果用户的猜测与随机数不相等,程序会判断猜测是太大了还是 太小,并给出相应的提示。
  4. 回到步骤3:如果用户猜错了,程序会再次提示用户输入猜测, 重复步骤4。

涉及知识点

  • 随机数引入
  • 用户输入
  • 循环判断

用 0 基础写出第一个 Rust 小游戏:猜数字(从 println! 到 match、loop、随机数)

目标:你将从零开始写出一个完整可运行的“猜数字”命令行小游戏,并在过程中理解每一段代码:println!、输入读取、字符串转数字、Result、match、随机数、比较、循环与退出。 本文解释遵循 Rust 官方书籍/官方标准库文档的概念与术语(如:宏、表达式、枚举、Result、match 表达式、可变引用等)。

你最终会写出什么?

运行程序后,终端会不断提示你输入数字:

  • 输入太小:提示 Too small

  • 输入太大:提示 Too big

  • 输入正确:结束游戏

(为了教学清晰,本文会把代码分成多步,每一步都能 cargo run 跑通。)

0. 准备工作:创建项目 + 添加依赖

0.1 创建 Rust 项目

在终端执行:

Terminal window
cargo new guess_number
cd guess_number

cargo 是 Rust 的构建工具与包管理器。它会生成一个可运行的项目结构,并创建 src/main.rs。

0.2 添加随机数库 rand

Rust 标准库不提供随机数生成器,所以我们使用常用的第三方 crate:rand。

Terminal window
cargo add rand

执行后,你会在 Cargo.toml 看到:

[dependencies]
rand = "..."

说明:你贴的代码使用了 rand::random_range(…),这是 rand 新版本提供的函数;很多旧教程(包括早期示例)会使用 thread_rng().gen_range(…)。本文会给出两种写法的说明,并优先给出更“通用、遇到坑更少”的方式。

1. 第一步:学会输出(println!)

打开 src/main.rs,先写最小程序:

fn main() {
println!("begin guess number");
}

1.1 println! 是什么?

println! 是 宏(macro),不是普通函数。

Rust 中宏名称后面带 !,宏在编译期展开,可以接收灵活的参数形式。

你可以这样输出变量:

let x = 10;
println!("x = {}", x);

这里 {} 是格式化占位符,把变量以默认格式插入字符串。

2. 第二步:读取用户输入(std::io、stdin、read_line)

现在我们让用户在终端输入一行内容。

use std::io;
fn main() {
println!("input guess number:");
let mut guess_str = String::new();
io::stdin()
.read_line(&mut guess_str)
.expect("Failed to read line");
println!("you typed: {}", guess_str);
}

2.1 use std::io; 是什么?

std::io 是 Rust 标准库里的 模块,提供输入输出相关的类型与函数。

use 的作用是把路径引入当前作用域,便于写 io::stdin(),而不是每次写全路径 std::io::stdin()。

2.2 String::new() 是什么?

String 是标准库提供的“可增长、拥有所有权”的字符串类型(内容在堆上)。

String::new() 创建一个空字符串。

2.3 为什么要 mut?

read_line 会把用户输入写进 guess_str,这意味着 guess_str 必须是可变的:let mut guess_str = …

2.4 &mut guess_str 是什么?

&mut 表示 可变引用(mutable reference)。

你把 guess_str 的可变引用“借给” read_line,允许它在不拿走所有权的情况下修改字符串内容。

2.5 read_line(…).expect(…) 做了什么?

read_line 会返回一个 Result:

  • 成功:Ok(读取到的字节数)

  • 失败:Err(错误信息)

expect(”…”) 的行为是:

  • 如果是 Ok(…):继续执行

  • 如果是 Err(…):直接让程序崩溃并显示你给的提示文字

新手阶段用 expect 很常见:因为它让你先把流程跑通,后面再学更严谨的错误处理。

3. 第三步:把字符串转换成数字(trim + parse + match)

真实游戏里,我们需要把输入从字符串变成数字。

3.1 为什么要 trim()?

read_line 通常会把换行符也读进来,例如你输入 42,字符串可能是 “42\n”。 trim() 会去掉首尾空白(包含换行),得到更干净的内容。

3.2 parse() 返回什么?

parse() 会把字符串解析成某个类型,但它不是“必然成功”的,所以它返回 Result:

  • Ok(解析后的值)

  • Err(解析失败的错误)

3.3 match 是什么?它会“返回”什么?

match 在 Rust 里是 match 表达式。 表达式的关键点:它会求值并产生一个结果。

因此你可以写:

let guess_num: i32 = match guess_str.trim().parse() {
Ok(num) => num,
Err(_) => 0,
};

上面这段的含义是:

如果解析成功,就把 num 作为整个 match 的结果

否则返回 0

在猜数字游戏中,我们不希望解析失败时“默认变成 0”,更合理的是“让用户重新输入”。因此使用 continue(进入下一轮循环)更合适:

let guess_num: i32 = match guess_str.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};

Ok(num) / Err(_):分别匹配 Result 的两个变体(枚举的 variants)

_:通配符,表示“我不关心这个值是什么”

continue:立刻结束本次循环,进入下一次循环(后面会讲 loop)

4. 第四步:生成随机数(rand + 范围 Range)

现在我们需要一个 1~100 的随机整数。

4.1 范围 1..101 是什么意思?

1..101 表示从 1 到 100(包含 1,不包含 101)

这符合猜数字常见设定:1~100

4.2 rand 的两种常见写法(看你用的是哪种 API)

写法 A:更通用(很多教程/示例都兼容)

use rand::Rng;
let random_num: i32 = rand::thread_rng().gen_range(1..101);

解释:

rand::thread_rng():获取线程本地随机数生成器

gen_range(1..101):在给定范围生成随机值

use rand::Rng;:因为 gen_range 是 Rng trait 提供的方法,需要把 trait 引入作用域

写法 B:新版提供的便捷函数

let random_num: i32 = rand::random_range(1..101);

解释:

这是 rand 新版提供的简写形式

实际底层等价于“拿到线程本地 RNG,再从范围生成随机值”的思路

如果你在编译时遇到 “找不到 random_range” 或 feature 相关报错,请改用写法 A(更稳妥),因为它在各种环境/教程版本里更常见。

5. 第五步:比较大小(cmp + Ordering + match)

我们要判断“猜小了/猜大了/猜对了”。

Rust 标准库里常见写法是:

use std::cmp::Ordering;
match guess_num.cmp(&random_num) {
Ordering::Less => println!("Too small number"),
Ordering::Greater => println!("Too big number"),
Ordering::Equal => println!("You win!"),
}

5.1 cmp 是什么?

cmp 是一个比较方法:比较两个值的大小关系,并返回 Ordering。

5.2 Ordering 是什么类型?

Ordering 是一个 枚举(enum),只有三种可能:

  • Less:小于

  • Equal:等于

  • Greater:大于

5.3 为什么 cmp(&random_num) 要加 &?

cmp 的参数是“另一个值的引用”。因此传 &random_num(对随机数的引用)符合方法签名要求。

6. 第六步:循环直到猜对(loop + continue + break)

最后把所有东西串起来:反复读输入 → 解析 → 比较 → 给反馈 → 猜对就退出。

完整版代码(教学注释版)

下面给出一份“清晰、可运行、注释够新手理解”的最终版本。我使用更通用的 rand 写法 A(更不容易被版本差异坑到)。你也可以替换成 random_range。

use rand::Rng; // 引入 Rng trait,才能用 gen_range
use std::cmp::Ordering; // 引入 Ordering 枚举,用于比较结果
use std::io; // 标准输入输出模块
fn main() {
println!("begin guess number");
// 生成 1..101 范围内的随机数(实际是 1~100)
let random_num: i32 = rand::thread_rng().gen_range(1..101);
// loop:无限循环,直到遇到 break
loop {
println!("input guess number");
// 用 String 保存用户输入
let mut guess_str = String::new();
// 从标准输入读取一行,写入 guess_str
io::stdin()
.read_line(&mut guess_str)
.expect("Failed to read line");
// trim 去掉首尾空白(包括换行符),parse 尝试解析成 i32
// parse 返回 Result,所以用 match 分情况处理
let guess_num: i32 = match guess_str.trim().parse() {
Ok(num) => num, // 成功:拿到解析出的数字
Err(_) => continue, // 失败:不是数字,重新开始下一轮循环
};
// cmp 比较两个数,返回 Ordering(Less/Greater/Equal)
match guess_num.cmp(&random_num) {
Ordering::Less => println!("Too small number"),
Ordering::Greater => println!("Too big number"),
Ordering::Equal => {
println!("Correct! Game over.");
break; // 猜对了:退出 loop
}
}
}
}

常见新手问题(非常关键)

1) 为什么 match 看起来像 switch,但还能赋值给变量?

因为 Rust 的 match 是表达式:它会产生一个值,所以能写 let x = match … { … };。

2) 为什么 parse 不直接给数字,还要 Ok/Err?

因为字符串不一定是合法数字(比如输入 abc)。Rust 用 Result 明确表达“可能失败”,这是一种强类型的错误处理方式。

3) 为什么每次循环都 String::new()?

因为每轮我们都要读一行新输入;新建一个空 String 最直观。你也可以复用同一个 String,但要记得清空内容(新手先不急)。

练习扩展(从入门到进阶的自然路线)

下面这些练习能让你对本例的每个概念更扎实:

  • 输入提示不换行:把 println! 改成 print!,并在读输入前 flush 一下(理解缓冲)。

  • 输入错误给提示:当 Err(_) 时打印一句 “Please input a number”。

  • 统计猜测次数:加一个 attempts 计数器,猜对时输出你猜了几次。

  • 限制输入范围:如果输入 <1 或 >100,提示“范围必须是 1~100”。