04-猜数游戏优化

前言

在上一章中,我们已经实现了猜数逻辑的核心功能,并通过这个猜数有些,熟悉了 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 呢?WhileLoop 有什么异同呢?

while & loop

相同点
  1. 都是用来执行重复的代码块,直到特定条件不再满足为止。
  2. 都可以使用控制流语句(如 breakcontinue)来控制循环的行为。
不同点
  1. 语法上的差异:loop 循环使用 loop 关键字,后跟一个代码块 {},而 while 循环使用 while 关键字,后跟一个条件表达式和一个代码块 {}
  2. 执行顺序:loop 循环会无限地执行循环体内的代码块,直到遇到 break 或其他终止条件。而 while 循环会在每次循环之前先检查条件是否满足,如果条件不满足,则跳出循环。
  3. 使用场景: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 是一个枚举类型,他有两个分支,也就是:OkErr,而我们上面使用的 expect ,就是在 Ok 的情况下,返回转换后的值,如果在 Err 的情况下,就会导致程序崩溃,并输出传入到 expect 中的提示文字。我们如果要优化的话,还得从这里做文章。

不知道大家是否还记得上一个章节我们使用 match 关键字用来执行 Cmp 的结果分支,而之所以 match 能够这么处理,就是因为 Cmp 返回的结果是 Ordering,而 Ordering 也是一个枚举类型:

而我们在后面的花括号中其实就是针对这个枚举类型的每一个分支进行处理。

相信看到这里,大家应该都已经知道了,我们这个程序应该怎么优化了吧。

没错,就是不用 expect 这个暴力处理方法,而是使用 match 关键字,然后再处理一下 OkErr 这两个枚举分支即可。

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 的方式处理错误。

结语

至此,我们已经将上一章节没有完成的连续猜测的功能分别使用 whileloop 都实现了一遍,并且使用 match 解决了我们程序的一个健壮性问题了,我们也已经将我们这个小游戏完成了。通过这个小游戏的开发,让我们初始了 Rust 世界当中的的一些奇妙特性。这之中有看起来跟 Js/Ts 很类似的特性,如:crate => npm包rust 的类型推论 => ts 的类型推论rust用 let 定义变量 => ts 用 let 定义变量 等等,当然也有很多跟 ts中不一样的,例如在 js 当中,用 let 定义的变量可以重新赋值但不能重新定义,但在 rust 当中,let 定义的变量不能重新赋值,除非你加上 mut 关键字,但可以重新定义(Shadowing)。

相信通过这个小程序的开发,你已经对其中的某一些特性已经有了一定的了解了。之后,我们就来正式进入 Rust 的语法程序时间,深入的学习让人着迷 Rust 吧。

相关推荐
龙湾开发1 小时前
轻量级高性能推理引擎MNN 学习笔记 02.MNN主要API
人工智能·笔记·学习·机器学习·mnn
HappyAcmen2 小时前
线代第二章矩阵第八节逆矩阵、解矩阵方程
笔记·学习·线性代数·矩阵
愚润求学4 小时前
【递归、搜索与回溯】专题一:递归(二)
c++·笔记·算法·leetcode
愚润求学4 小时前
【Linux】基础 IO(一)
linux·运维·服务器·开发语言·c++·笔记
Wallace Zhang4 小时前
STM32F103_LL库+寄存器学习笔记22 - 基础定时器TIM实现1ms周期回调
笔记·stm32·学习
大白的编程日记.5 小时前
【Linux学习笔记】理解一切皆文件实现原理和文件缓冲区
linux·笔记·学习
孞㐑¥5 小时前
Linux之进程控制
linux·开发语言·c++·经验分享·笔记
一丝晨光5 小时前
数值溢出保护?数值溢出应该是多少?Swift如何让整数计算溢出不抛出异常?类型最大值和最小值?
java·javascript·c++·rust·go·c·swift
景天科技苑5 小时前
【Rust泛型】Rust泛型使用详解与应用场景
开发语言·后端·rust·泛型·rust泛型
Alessio Micheli5 小时前
奇怪的公式
笔记·线性代数