前言
在上一章中,我们已经实现了猜数逻辑的核心功能,并通过这个猜数有些,熟悉了 Rust 的部分语法特性,但我们也留下了一点尾巴,在这一章,我们就来优化一下这个游戏的一些体验和逻辑,把它完善好。并通过这些优化,进一步地了解 Rust 的语法特性。
实现连续猜测
我们上一章留下来没完成的功能主要是用户在输入错误之后,程序就直接终止了,无法连续猜测。如果在 js/ts
当中实现这个程序的话,相信所有前端同学应该都能够信手拈来,就是用 while
循环一直执行输入输出程序,直到猜错时 break
掉。
那么,在 Rust 当中,我们应该如何写一个无限循环呢?还是用 while
吗?
话不多说,我们先来按照 JS/TS 思维实现一个试试看就知道了。
While
typescript
// prelude 是 Rust 中的预导入模块,它会自动引入一些常用的模块
// 如果不引入的话,下面的 println!() 会报错
// 引用标准库中的 io 模块,不引用的话下面的 io::stdin() 会报错
use std::io;
// 引入随机函数库
use rand::Rng;
// 大小比较函数库
use std::cmp::Ordering;
fn main() {
// 生成一个 1 ~ 100 的随机数
// rand::thread_rng() 生成一个随机数生成器
// gen_range() 生成一个指定范围的随机数
// .. 表示一个范围,如 1..100 表示 1 ~ 100
// 生成的随机数是一个闭区间,即包含 1 和 100
// 如果想要生成一个开区间,即不包含 1 和 100,可以使用 ..= ,如 1..=100
let secret_number = rand::thread_rng().gen_range(1..100);
println!("生成的随机数是:{}", secret_number);
// 定义一个用于进入循环的标志,这个标志应该是可变的,因为我们需要在猜对的时候更改这个标记,让程序终止循环,结束程序。
let mut flag = true;
// 循环猜数
while flag {
println!("猜数游戏,请输入一个猜测的数:");
// 定义一个可变的空字符串用于接收用户输入的值
// mut 表示可变的,因为在 rust 当中,所有的变量默认是不可变的(immutable)
// 如果不加 mut 的话,当我们要去改变 guess 的值时,编译器会报错
// 如:let a = 1;
// a = 2; // 这里会报错,因为 a 是不可变的
// 如果加上 mut 的话,就可以改变变量的值了
// 如:let mut a = 1;
// a = 2; // 这里就不会报错了
let mut guess =
// :: 表示 new 是 String 类型的一个关联函数,就相当于在 JS 当中的静态方法,
// 不是针对某个示例的方法,而是针对整个类的方法
String::new();
// 使用 io::stdin() 从标准输入句柄(handle)中读取用户输入的值
io::stdin()
// 此处使用的符号是 . ,表示调用 stdin() 方法返回的对象的方法
// 与 JS 中的调用实例方法的方式一样。
// read_line() 读取用户输入的值并将其赋值给上面定义的 guess 变量
// 需要特别注意的是,我们传过来的是一个可变的引用类型,因此需要加上 &mut
.read_line(&mut guess)
// 如果读取失败,程序会崩溃并打印错误信息
.expect("读取行失败");
// 当然,如果我们上面不使用 use std::id; 的话,我们也可以向下面这样使用
// std::io::stdin()
// 打印用户输入的值
// {} 表示占位符,类似于 ES6 中的 ${}
// 后面的 guess 是一个变量,输出时会将其值替换到占位符中
println!("你猜测的数是:{}", guess);
// 由于我们后面需要进行数字的比较,但用户输入的 guess 实际是一个字符串来的,
// rust 不同于 js,rust 是一个静态的请类型语言,我们不能将一个字符串跟一个数字进行比较
// 因此,我们需要将 guess 转换成数字类型
// 转化方法如下:
// 1. 使用 guess.trim() 去除用户输入的值两边的空格
// 2. 使用 guess.parse() 将其转换成数字类型
// 3. 捕获可能出现的错误,如果出现错误,则程序会崩溃并打印错误信息
// 此外,在 js 当中,我们无法使用 let 变量定义一个重名的变量,我们 32 行当中已经声明了一个 guess 变量
// 但这里我们又在使用 let 定义了一个同名的变量 guess,区别仅仅是类型不同,一个是字符串,一个是整数
// 这就引申出 Rust 中的一个重要的概念:Shadowing,即变量遮蔽
// Shadowing 的作用是,我们可以使用 let 关键字定义一个同名的变量,这样做的好处是,我们可以重复使用同一个变量名
// 这个特性通常使用在需要进行类型转换的时候,如这里,我们需要将 guess 从字符串转换成数字,因此我们使用 let 定义了一个同名的变量
let guess: i64 = guess.trim().parse().expect("请输入一个数字");
// 比较大小
// match 的作用:
// match 语句正在对 guess.cmp(&secret_number) 的结果进行匹配。
// cmp 是一个比较方法,它返回一个 Ordering 枚举,该枚举有三个可能的值:Less,Greater 和 Equal。
// match 语句在这里用于根据 guess 和 secret_number 的比较结果,执行不同的输出操作
match guess.cmp(&secret_number) {
// 如果用户输入的值小于随机数,则输出 Too small
Ordering::Less => println!("Too small!"),
// 如果用户输入的值大于随机数,则输出 Too big
Ordering::Greater => println!("Too big!"),
// 如果用户输入的值等于随机数,则输出 You win
Ordering::Equal => {
println!("You win!");
// 当用户猜对时,将标记改为 false,程序终止循环,结束程序
flag = false;
},
}
}
}
上面这个程序实现的思路我们就不再说了,跟 JS/TS 没有设么差别,就是定一个可变的布尔型标记 flag
,初始的时候为 true
,并将我们上一章实现的逻辑代码都放到 while flag {...}
当中,当我们猜对时,再将这个标记设置为 false
,这样,就可以终止循环,结束程序了。我们来运行一下,看看程序表现是否符合预期呢?

事实证明,上面的程序确实也是符合我们预期的。
Loop
其实,在 Rust 当中,不仅仅可以使用 while
循环实现上述的逻辑,在 rust
当中,还提供了另一种方式,叫做:loop
,这个关键字是 Rust 中专门用于无限循环的关键字。那么,既然已经支持了 While 了,为啥还要 Loop 呢?While 和 Loop 有什么异同呢?
while & loop
相同点
- 都是用来执行重复的代码块,直到特定条件不再满足为止。
- 都可以使用控制流语句(如
break
和continue
)来控制循环的行为。
不同点
- 语法上的差异:
loop
循环使用loop
关键字,后跟一个代码块{}
,而while
循环使用while
关键字,后跟一个条件表达式和一个代码块{}
。 - 执行顺序:
loop
循环会无限地执行循环体内的代码块,直到遇到break
或其他终止条件。而while
循环会在每次循环之前先检查条件是否满足,如果条件不满足,则跳出循环。 - 使用场景:
loop
循环通常用于需要无限执行或者在特定条件下才能跳出的情况,例如游戏循环或事件循环。而while
循环通常用于在满足特定条件时才执行代码块的情况。
使用 loop 实现程序
rust
// prelude 是 Rust 中的预导入模块,它会自动引入一些常用的模块
// 如果不引入的话,下面的 println!() 会报错
// 引用标准库中的 io 模块,不引用的话下面的 io::stdin() 会报错
use std::io;
// 引入随机函数库
use rand::Rng;
// 大小比较函数库
use std::cmp::Ordering;
fn main() {
// 生成一个 1 ~ 100 的随机数
// rand::thread_rng() 生成一个随机数生成器
// gen_range() 生成一个指定范围的随机数
// .. 表示一个范围,如 1..100 表示 1 ~ 100
// 生成的随机数是一个左闭右开区间,即包含 1 但不包含 100
// 如果想要生成一个闭区间,即包含 1 和 100,可以使用 ..= ,如 1..=100
let secret_number = rand::thread_rng().gen_range(1..100);
println!("生成的随机数是:{}", secret_number);
// 无限循环猜数
loop {
println!("猜数游戏,请输入一个猜测的数:");
// 定义一个可变的空字符串用于接收用户输入的值
// mut 表示可变的,因为在 rust 当中,所有的变量默认是不可变的(immutable)
// 如果不加 mut 的话,当我们要去改变 guess 的值时,编译器会报错
// 如:let a = 1;
// a = 2; // 这里会报错,因为 a 是不可变的
// 如果加上 mut 的话,就可以改变变量的值了
// 如:let mut a = 1;
// a = 2; // 这里就不会报错了
let mut guess =
// :: 表示 new 是 String 类型的一个关联函数,就相当于在 JS 当中的静态方法,
// 不是针对某个示例的方法,而是针对整个类的方法
String::new();
// 使用 io::stdin() 从标准输入句柄(handle)中读取用户输入的值
io::stdin()
// 此处使用的符号是 . ,表示调用 stdin() 方法返回的对象的方法
// 与 JS 中的调用实例方法的方式一样。
// read_line() 读取用户输入的值并将其赋值给上面定义的 guess 变量
// 需要特别注意的是,我们传过来的是一个可变的引用类型,因此需要加上 &mut
.read_line(&mut guess)
// 如果读取失败,程序会崩溃并打印错误信息
.expect("读取行失败");
// 当然,如果我们上面不使用 use std::id; 的话,我们也可以向下面这样使用
// std::io::stdin()
// 打印用户输入的值
// {} 表示占位符,类似于 ES6 中的 ${}
// 后面的 guess 是一个变量,输出时会将其值替换到占位符中
println!("你猜测的数是:{}", guess);
// 由于我们后面需要进行数字的比较,但用户输入的 guess 实际是一个字符串来的,
// rust 不同于 js,rust 是一个静态的请类型语言,我们不能将一个字符串跟一个数字进行比较
// 因此,我们需要将 guess 转换成数字类型
// 转化方法如下:
// 1. 使用 guess.trim() 去除用户输入的值两边的空格
// 2. 使用 guess.parse() 将其转换成数字类型
// 3. 捕获可能出现的错误,如果出现错误,则程序会崩溃并打印错误信息
// 此外,在 js 当中,我们无法使用 let 变量定义一个重名的变量,我们 32 行当中已经声明了一个 guess 变量
// 但这里我们又在使用 let 定义了一个同名的变量 guess,区别仅仅是类型不同,一个是字符串,一个是整数
// 这就引申出 Rust 中的一个重要的概念:Shadowing,即变量遮蔽
// Shadowing 的作用是,我们可以使用 let 关键字定义一个同名的变量,这样做的好处是,我们可以重复使用同一个变量名
// 这个特性通常使用在需要进行类型转换的时候,如这里,我们需要将 guess 从字符串转换成数字,因此我们使用 let 定义了一个同名的变量
let guess: i64 = guess.trim().parse().expect("请输入一个数字");
// 比较大小
// match 的作用:
// match 语句正在对 guess.cmp(&secret_number) 的结果进行匹配。
// cmp 是一个比较方法,它返回一个 Ordering 枚举,该枚举有三个可能的值:Less,Greater 和 Equal。
// match 语句在这里用于根据 guess 和 secret_number 的比较结果,执行不同的输出操作
match guess.cmp(&secret_number) {
// 如果用户输入的值小于随机数,则输出 Too small
Ordering::Less => println!("Too small!"),
// 如果用户输入的值大于随机数,则输出 Too big
Ordering::Greater => println!("Too big!"),
// 如果用户输入的值等于随机数,则输出 You win
Ordering::Equal => {
println!("You win!");
// 当用户猜对时,使用 break 关键字程序终止循环,结束程序
break;
},
}
}
}
上面程序的运行结果也不出意料的也是符合预期的:

提升程序的健壮性
实现到上面,我们的程序看似已经能够很好的完成我们猜数游戏的需求了,但实际上,还有一些细节处理没有到位,例如,我们在讲用户输入的字符串转换成数字时,如果用户输入的字符串无法转换成数字,如,用户输入了:one
,由于我们使用了 expect
的方式处理异常。使用这种方式,如果正常转换的话,expect
方法会直接返回转换后的值,但如果转换异常,将会直接终止程序并输出 expect
中错误信息。

我们在开发一个应用程序的时候,肯定不能说用户输入了一个非法的内容就直接崩溃是不是,这也太拉跨了吧。所以,我们还得想办法解决这个问题。
我们先来看一下 parse
这个方法,究竟返回的事一个什么东西。

我们可以看到 parse
返回的是一个 Result
类型的值,那么,Result
怎样的呢?,我们继续点进去看看:

我们可以看到,原来这个 Result
是一个枚举类型,他有两个分支,也就是:Ok
和 Err
,而我们上面使用的 expect
,就是在 Ok
的情况下,返回转换后的值,如果在 Err
的情况下,就会导致程序崩溃,并输出传入到 expect
中的提示文字。我们如果要优化的话,还得从这里做文章。
不知道大家是否还记得上一个章节我们使用 match
关键字用来执行 Cmp
的结果分支,而之所以 match
能够这么处理,就是因为 Cmp
返回的结果是 Ordering
,而 Ordering
也是一个枚举类型:


而我们在后面的花括号中其实就是针对这个枚举类型的每一个分支进行处理。
相信看到这里,大家应该都已经知道了,我们这个程序应该怎么优化了吧。
没错,就是不用 expect
这个暴力处理方法,而是使用 match
关键字,然后再处理一下 Ok
和 Err
这两个枚举分支即可。
rust
// prelude 是 Rust 中的预导入模块,它会自动引入一些常用的模块
// 如果不引入的话,下面的 println!() 会报错
// 引用标准库中的 io 模块,不引用的话下面的 io::stdin() 会报错
use std::io;
// 引入随机函数库
use rand::Rng;
// 大小比较函数库
use std::cmp::Ordering;
fn main() {
// 生成一个 1 ~ 100 的随机数
// rand::thread_rng() 生成一个随机数生成器
// gen_range() 生成一个指定范围的随机数
// .. 表示一个范围,如 1..100 表示 1 ~ 100
// 生成的随机数是一个左闭右开区间,即包含 1 但不包含 100
// 如果想要生成一个闭区间,即包含 1 和 100,可以使用 ..= ,如 1..=100
let secret_number = rand::thread_rng().gen_range(1..100);
println!("生成的随机数是:{}", secret_number);
// 无限循环猜数
loop {
println!("猜数游戏,请输入一个猜测的数:");
// 定义一个可变的空字符串用于接收用户输入的值
// mut 表示可变的,因为在 rust 当中,所有的变量默认是不可变的(immutable)
// 如果不加 mut 的话,当我们要去改变 guess 的值时,编译器会报错
// 如:let a = 1;
// a = 2; // 这里会报错,因为 a 是不可变的
// 如果加上 mut 的话,就可以改变变量的值了
// 如:let mut a = 1;
// a = 2; // 这里就不会报错了
let mut guess =
// :: 表示 new 是 String 类型的一个关联函数,就相当于在 JS 当中的静态方法,
// 不是针对某个示例的方法,而是针对整个类的方法
String::new();
// 使用 io::stdin() 从标准输入句柄(handle)中读取用户输入的值
io::stdin()
// 此处使用的符号是 . ,表示调用 stdin() 方法返回的对象的方法
// 与 JS 中的调用实例方法的方式一样。
// read_line() 读取用户输入的值并将其赋值给上面定义的 guess 变量
// 需要特别注意的是,我们传过来的是一个可变的引用类型,因此需要加上 &mut
.read_line(&mut guess)
// 如果读取失败,程序会崩溃并打印错误信息
.expect("读取行失败");
// 当然,如果我们上面不使用 use std::id; 的话,我们也可以向下面这样使用
// std::io::stdin()
// 打印用户输入的值
// {} 表示占位符,类似于 ES6 中的 ${}
// 后面的 guess 是一个变量,输出时会将其值替换到占位符中
println!("你猜测的数是:{}", guess);
// 由于我们后面需要进行数字的比较,但用户输入的 guess 实际是一个字符串来的,
// rust 不同于 js,rust 是一个静态的请类型语言,我们不能将一个字符串跟一个数字进行比较
// 因此,我们需要将 guess 转换成数字类型
// 转化方法如下:
// 1. 使用 guess.trim() 去除用户输入的值两边的空格
// 2. 使用 guess.parse() 将其转换成数字类型
// 3. 捕获可能出现的错误,如果出现错误,则程序会崩溃并打印错误信息
// 此外,在 js 当中,我们无法使用 let 变量定义一个重名的变量,我们 32 行当中已经声明了一个 guess 变量
// 但这里我们又在使用 let 定义了一个同名的变量 guess,区别仅仅是类型不同,一个是字符串,一个是整数
// 这就引申出 Rust 中的一个重要的概念:Shadowing,即变量遮蔽
// Shadowing 的作用是,我们可以使用 let 关键字定义一个同名的变量,这样做的好处是,我们可以重复使用同一个变量名
// 这个特性通常使用在需要进行类型转换的时候,如这里,我们需要将 guess 从字符串转换成数字,因此我们使用 let 定义了一个同名的变量
let guess: i64 = match guess.trim().parse() {
// 如果转换成功,则将其值赋值给 guess
Ok(num) => num,
// 如果转换失败,则使用 continue 关键字跳过本次循环,继续下一次循环,让用户重新输入
Err(_) => {
println!("请输入一个数字!");
continue;
},
};
// 比较大小
// match 的作用:
// match 语句正在对 guess.cmp(&secret_number) 的结果进行匹配。
// cmp 是一个比较方法,它返回一个 Ordering 枚举,该枚举有三个可能的值:Less,Greater 和 Equal。
// match 语句在这里用于根据 guess 和 secret_number 的比较结果,执行不同的输出操作
match guess.cmp(&secret_number) {
// 如果用户输入的值小于随机数,则输出 Too small
Ordering::Less => println!("Too small!"),
// 如果用户输入的值大于随机数,则输出 Too big
Ordering::Greater => println!("Too big!"),
// 如果用户输入的值等于随机数,则输出 You win
Ordering::Equal => {
println!("You win!");
// 当用户猜对时,使用 break 关键字程序终止循环,结束程序
break;
},
}
}
}

这样,我们就解决了这个程序健壮性的问题了。
使用 match
关键字来代替 expect
处理错误,是在 Rust 中处理错误的惯用手段 。就类似于 Js/Ts 当中使用 try...catch
的方式处理错误。
结语
至此,我们已经将上一章节没有完成的连续猜测的功能分别使用 while
和 loop
都实现了一遍,并且使用 match
解决了我们程序的一个健壮性问题了,我们也已经将我们这个小游戏完成了。通过这个小游戏的开发,让我们初始了 Rust 世界当中的的一些奇妙特性。这之中有看起来跟 Js/Ts 很类似的特性,如:crate => npm包
、rust 的类型推论 => ts 的类型推论
、rust用 let 定义变量 => ts 用 let 定义变量
等等,当然也有很多跟 ts
中不一样的,例如在 js 当中,用 let
定义的变量可以重新赋值但不能重新定义,但在 rust
当中,let 定义的变量不能重新赋值,除非你加上 mut
关键字,但可以重新定义(Shadowing)。
相信通过这个小程序的开发,你已经对其中的某一些特性已经有了一定的了解了。之后,我们就来正式进入 Rust 的语法程序时间,深入的学习让人着迷 Rust 吧。