现在让我们一起用 Rust 来完成一个项目,即猜数字游戏:游戏开始后会提示用户输入数字,如果数字比生成的随机数大,那么就提示太大了,反之提示数字太小了,如果数字相同,则输出庆祝信息,然后退出游戏。
创建一个新项目
$ cargo new guesssing_game
$ cd guessing_game
Cargo.toml
[package]
name = "guessing_game"
version = "0.1.0"
edition = "2021"
[dependencies]
首先打开 Cargo 项目配置文件,可以看到项目名称,项目版本和 Rust 版本信息。
main.rs
在默认的程序文件中,一如既往还是 Cargo 默认的程序:
fn main() {
println!("Hello, world!");
}
我们接下来可以运行cargo run
命令来编译运行项目。
获取用户输入
既然是让用户来猜数字的游戏,那么我们首先需要获取用户的输入:
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;
首先,我们需要引入标准输入/输出库,来处理用户的输入/输出。
然后,创建主函数main
来存放游戏的逻辑,在程序开始后分别打印:
Guess the number!
和
Please input your guess.
内容。
这段内容要求用户在游戏开始时输入猜想的数字。
存储用户的输入
let mut guess = String::new();
我们用这行代码来存储用户的输入。
你可能注意到,我们用let mut
来定义一个新变量,这里let
关键字用来声明变量,而mut
关键字表示声明的变量是可修改(mutable)的。例如:
let number = 10;
这里声明的number
就是一个不可变变量(immutable),可以理解为 C 语言中的 const
变量。
这也是 Rust 语言在内存管理方面更严格的体现,正是这种严格的内存管理,使得 Rust 项目更少出现程序内存错误,这也是我们使用 Rust 语言的重要理由。
String::new()
回到主程序当中
let mut guess = String::new();
我们在这里调用了String::new()
函数,来对guess
变量赋值;这个函数返回一个字符串实例,而这个实例允许我们将字符串内容赋值给guess
变量。
接收用户输入
io::stdin()
.read_line(&mut guess)
注:
如果我们没有在程序文件开头用use
引入io
标准库,我们仍旧可以使用std::io::stdin
来接收用户的输入。
std::io::stdin().read_line(&mut guess).expect("Failed to read line.");
如果在代码中不使用错误处理expect
,程序编译时会触发警告。
生成随机数
我们在这里需要生成 0-100 的随机数。首先我们需要引入依赖:
打开Cargo.toml
文件
[dependencies]
rand = "0.8.5"
依赖项目中,指定了版本号为:0.8.5
的依赖项。添加保存文件后,我们重新构建项目,Cargo 会自动帮我们解决所有依赖:
cargo build
Updating crates.io index
Locking 15 packages to latest compatible versions
Adding byteorder v1.5.0
Adding cfg-if v1.0.0
Adding getrandom v0.2.15
Adding libc v0.2.159
Adding ppv-lite86 v0.2.20
Adding proc-macro2 v1.0.86
Adding quote v1.0.37
Adding rand v0.8.5
Adding rand_chacha v0.3.1
Adding rand_core v0.6.4
Adding syn v2.0.77
Adding unicode-ident v1.0.13
Adding wasi v0.11.0+wasi-snapshot-preview1 (latest: v0.13.2+wasi-0.2.1)
Adding zerocopy v0.7.35
Adding zerocopy-derive v0.7.35
Downloaded libc v0.2.159
Downloaded 1 crate (755.4 KB) in 0.69s
Compiling proc-macro2 v1.0.86
Compiling unicode-ident v1.0.13
Compiling libc v0.2.159
Compiling cfg-if v1.0.0
Compiling byteorder v1.5.0
Compiling getrandom v0.2.15
Compiling rand_core v0.6.4
Compiling quote v1.0.37
Compiling syn v2.0.77
Compiling zerocopy-derive v0.7.35
Compiling zerocopy v0.7.35
Compiling ppv-lite86 v0.2.20
Compiling rand_chacha v0.3.1
Compiling rand v0.8.5
Compiling guessing_game v0.1.0 (/Users/moker/Projects/Rust-Learning/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 7.40s
配置文件添加依赖之后,我们需要在代码中引入:
use rand::Rng;
生成随机数:
let serect_number = rand::thread_rng().gen_range(1..=100); // 1..=100 代表从 1 到 100 的范围
将随机数和用户输入的数字进行比较
use std::cmp::Ordering;
// ...
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small.");
Ordering::Greater => println!("Too big.");
Ordering::Equal => println!("You win!");
}
我们添加了 5 行代码,用来比较随机数和用户输入。
首先我们从标准库中引入了:cmp::Ordering
,Ordering 是一种枚举类型,其提供了 Less ,Greater,Equal,用来表示cmp
方法的结果。
match 是 Rust 提供的一种表达式,表达式由分支(arm)构成,一个分支包含一个用于匹配的模式(pattern)。match 的值与分支模式相匹配时,Rust 会执行对应分支的代码,在这里,如果用户输入与随机数相同,match 会执行匹配模式中,代表相同的代码,而不会执行其他。模式和分支是 Rust 中强大的功能,它体现了代码可能遇到的多种情形,并确保你没有遗漏任何处理。详细内容将在后续介绍。
我们现在检查代码,会发现报错:
这里,报错是因为 guess 变量是字符串类型,而 secret_number 变量是数字(i32,32位有符号整数)类型,类型不同,不能比较。所以,我们需要将 guess 变量转换为数字类型。
i32
, a 32-bit number;u32
, an unsigned 32-bit number;i64
, a 64-bit number;
将字符串类型转换为数字类型:
let guess: u32 = guess.trim().parse().expect("Please type a number!");
你可能注意到,我们已经在之前的代码中声明了一个名为 guess 的变量,为什么又重新定义了一个相同名字的变量?这是因为 Rust 允许使用重新定义变量的方式,来遮蔽(shadow)之前的值,让我们可以复用变量,而不是被迫重新创建变量。在这里,我们只需要知道,这是一种在 Rust 中转换变量类型很常用的方式。
guess 为原始变量,trim 表示去除首尾的空白字符,例如'\0‘,‘\n',只留下用户输入的内容。字符串的 parse 方法将字符串解析成数字,如果报错,则输出:"Please type a number!"。因为 parse 方法只能在逻辑上将字符串转换为数字,而并非将字符转换为 ASCII 码,所以需要进行错误处理。
补全游戏逻辑
太棒了,现在游戏已经能玩了,但是如果运行,你会注意到,游戏只会运行一次,所以我们将游戏逻辑包含在循环中。
添加循环
loop {
let mut guess = String::new();
println!("Please input your guess.");
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line.");
let guess: u32 = guess.trim().parse().expect("Please type a number!");
println!("You guessed: {}", guess);
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small."),
Ordering::Greater => println!("Too big."),
Ordering::Equal => println!("You win!"),
}
}
虽然用户随时能通过 Ctrl-c 来终止程序,但这不是一种优雅的方法,因为我们在代码中添加游戏结束逻辑。
Ordering::Equal => {
println!("You win!");
break;
}
处理无效输入
尽管我们的游戏已经很完善,但是如果我们尝试输入字符,程序会报错并退出,然而这并不是我们希望看到的,所以,我们现在处理无效输入,并告知用户必须输入数字。
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => {
println!("Please type a number!");
continue;
},
};
我们将原来的 expect 调用换成 match 语句,因为 parse 方法会返回一个 Result 类型,而 Result 类型是一个包含 Ok 或 Err 成员的枚举。这里,match 模式中包含有两种分支,即 Ok 和 Err,match 将执行与 parse 方法返回值相同的分支。
Ok(num) => num
代表 match 将返回 guess 对应的数字作为结果赋值给 guess,
Err(_) => continue
代表 match 将返回 continue 作为结果,并触发程序跳过,其中,_
是一个通配符,不论 Err 中包含有任何信息,都将执行第二个分支的动作,continue 意味着跳过此次循环,由此就避免了 parse 报错。
现在,我们完成了游戏的所有内容!