03-猜数游戏

前言

通过之前的学习,我们已经搭建好了 Rust 的环境,并且学会了如何使用 Cargo 这个构建与包管理工具,接下来,就通过一个实际的程序示例来正式学习一下 Rust 的一些常见的语法,如:letmatch 等方法的作用以及使用方式,以及外部的 crate 的相关介绍。

目标

我们要使用 Rust 实现一个猜数游戏,游戏的细节如下:

  • 生成一个 1 ~ 100 的随机整数
  • 命令行当中提示用户输入一个他猜测的数字
  • 用户猜完之后,若用户猜的数与生成的随机数不一致,则程序提示用户猜测的数字太大了还是太小了。
  • 如果用户猜测的数字等于生层的这个随机数,则提示猜对并终止程序。

关联知识

Crate

直译过来的意思是"箱",顾名思义,crateRust 当中就是代表的是:第三方依赖 。这个类比成我们熟悉的 NodeJs 项目中的概念,其实就是我们的 NPM 包,也就是我们在 package.json 的依赖列表中枚举的这些第三方依赖。关于 create 的相关知识,我们后续再深入学习,在这里,我们只要把它类比成 Typescript 中的 npm包即可。

编写代码

创建项目

首先我们先创建一个新的 Rust 项目用于本次演示:

bash 复制代码
# 更多关于新建 Rust 项目的内容我们已经在上一篇文章中有详细介绍了,这里就不再赘述了,不清楚的同学可以到: 看看。
cargo new guessing_game

实现输入与输出

rust 复制代码
// src/main.rs
// prelude 是 Rust 中的预导入模块,它会自动引入一些常用的模块
// 如果不引入的话,下面的 println!() 会报错

// 引用标准库中的 io 模块,不引用的话下面的 io::stdin() 会报错
use std::io;

fn main() {
    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() 从标准输入句柄中读取用户输入的值
    io::stdin()
        // read_line() 读取用户输入的值并将其赋值给上面定义的 guess 变量
        // 需要特别注意的是,我们传过来的是一个可变的引用类型,因此需要加上 &mut
  			// 使用 & 也就是这里用的是地址的引用,也就是上面字符串定义时的 guess,以及此处的 guess,以及后面用于打印的 guess
  			// 他们都会指向同一个内存地址。
        .read_line(&mut guess)
        // 如果读取失败,程序会崩溃并打印错误信息
        .expect("读取行失败");
	  // 当然,如果我们上面不使用 use std::id; 的话,我们也可以向下面这样使用
    // std::io::stdin()

    // 打印用户输入的值
    // {} 表示占位符,类似于 ES6 中的 ${}
    // 后面的 guess 是一个变量,输出时会将其值替换到占位符中
    println!("你猜测的数是:{}", guess);
}

引入第三发依赖

Rust 的标准库里面,并没有支持生成一个随机数的方法,但 Rust 团队为我们提供了一个第三方依赖包 rand,我们可以在这个 crates.io 地方查找想要使用的库的一些信息,如版本信息、安装方法和使用方法等等。我们直接在这里搜索 rand,然后根据上面的指引使用:

bash 复制代码
# 这个跟前端领域的 yarn 安装依赖包的命名很像,都是使用 add 进行安装,并且会将依赖信息写入到 package.json(Cargo.toml)和yarn.lock(Cargo.toml)当中
cargo add rand

安装好后,我们查看一下 Cargo.toml,就可以看到在依赖项当中多了一个 rand 的依赖项了。

生成随机数

我们引入好了第三方依赖库 rand 之后,接下来就利用这个库提供的方法生成一个:1 ~ 100 的随机数:

rust 复制代码
// prelude 是 Rust 中的预导入模块,它会自动引入一些常用的模块
// 如果不引入的话,下面的 println!() 会报错

// 引用标准库中的 io 模块,不引用的话下面的 io::stdin() 会报错
use std::io;
// 引入随机函数库
use rand::Rng;


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);
  
    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);
}

生成随机数的细节和相关解释在上面的代码里面已经说的很清楚了,这边就不再赘述了,如果还有其他不清楚的,可以查看 rand 库的的定义:

比较大小

现在我们已经生成了一个 1 ~ 100 的随机整数,并且也通过接收用户的输入拿到了用户猜测的数字了,接下来就可以将这两个数进行比较了。

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);

    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: u32 = 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!"),
    }

}

由于 Rust 不同与 js ,它是一个静态的强类型语言,我们不能将一个字符串跟一个数字进行比较。因此,我们在比较之前,需要将 用户输入的字符串类型的 guess 转换为整数类型才能进行比较。

我们要比较两个数的大小关系,还需要引入一个标准库:use std::cmp::Ordering;,这个库是一个枚举类型,有:LessGreaterEqual 这三个枚举值,我们只需要按照上面代码的方式,对匹配到的每一个枚举值进行特异性处理即可。至于前面的 match 关键字,在 Rust 当中是一个非常重要且常见的关键字,我们以后在扩充学习一下。在这里,我们就只需要知道,match 会根据 guess.cmp 的结果枚举执行不同的操作即可。

上面的代码输出如下:

Rust 中的类型推论

或许有些细心的同学会发现,在我们还没有写 match 匹配方法时,我们上面的 secret_number 的类型是 i32 的,也就是 32 位的整数:

而当我们写了 match 相关的匹配语句时,上面的 secret_number 的类型竟然神奇地变成了 u32:

那么,究竟什么原因,导致 secret_number 类型反复横跳呢?其实原因在于我们的 gen_range(1..100) 方法产生的随机整数的类型其实不唯一,i32u32i64 都能够产生 1 ~ 100 的随机数,在我们还没有使用这个变量时,这个方法默认返回的类型是 i32,但当我们在后面进行匹配时,因为我们将 guess 的类型显示声明是 u32 ,因此触发了 Rust 的类型推论机制,Rust 认为,既然你这里需要个 u32 类型的数字进行比较,而我的 secret_number 可以是 i32u32i64 这三种类型中的一种,发现有u32类型,所以 Rust 的类型系统就认为,secret_number 应该是一个 u32 类型。

其实我们也可以做一个实验,如果我们把上面 66 行的代码中,guess 的类型从 u32 改成 i64

我们可以看到,上面的类型,又变成了 i64,这就可以证明我们上面所说了,secret_number 的类型是根据与他进行比较的 guess 的类型推论出来的,如果没有后面的比较操作,那么secret_number 默认是 i32 的类型。

其实这个我们熟悉的 Typescript 中的 类型推论系统是很像的:

typescript 复制代码
// 示例函数,接受一个数组参数并返回该数组的第一个元素
function getFirstElement<T>(arr: T[]): T {
  return arr[0];
}

// 类型推论:变量 `numbers` 的类型被推断为 `number[]`
const numbers = [1, 2, 3, 4, 5];

// 类型推论:变量 `firstNumber` 的类型被推断为 `number`
const firstNumber = getFirstElement(numbers);

console.log(`The first number in the array is: ${firstNumber}`);

就像上面的这个代码

我们可以看到 getFirstElement 方法实际上的参数是一个泛型参数,但当我们将 numbers 传进去时,由于numbers 根据 ts 的类型推论,得出类型是 number[] 类型,因此,getFirstElement 方法的参数和返回值也会根据传入的参数动态地改变了。由此也可以看出 RustTypescript 在设计上有很多异曲同工之妙。

结语

至此,我们已经使用 Rust 实现了一个简易版本的 猜数游戏,虽然这个游戏还有不少需要优化的点,比如:输入完无论正确还是错误程序就直接结束了,没办法一直猜测,直到最终正确。这个受限于文章的篇幅,以及本文涉及到的相关的知识点比较多,我们放在下一篇再继续优化完善。

在上面编写这个游戏的过程中,相信前端同学都会觉得,Rust 有很多跟 js/ts 很像的点,但有一些点又不一样。没错的,Rust 在设计层面上,可以说是博采众长,你总能发现他的语法里面的某些点,跟某些语言相似,这也是 Rust 的魅力之一。

我们通过本章的学习,了解了以下知识点:

  • Crate 的基本概念
  • 如何在 Rust 中引入第三方依赖包
  • 如何接受用户的输入
  • 如何生成一个随机数
  • 如何进行类型转换
  • Rust 中的类型推论
  • 如何进行数值比对

可以说,这个程序是:"麻雀虽小五脏俱全"。带我们逐步深入的了解到了 Rust 的一些语法细节。

相关推荐
齐 飞24 分钟前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb
丫头,冲鸭!!!1 小时前
B树(B-Tree)和B+树(B+ Tree)
笔记·算法
听忆.1 小时前
手机屏幕上进行OCR识别方案
笔记
Selina K2 小时前
shell脚本知识点记录
笔记·shell
2 小时前
开源竞争-数据驱动成长-11/05-大专生的思考
人工智能·笔记·学习·算法·机器学习
霍格沃兹测试开发学社测试人社区3 小时前
软件测试学习笔记丨Flask操作数据库-数据库和表的管理
软件测试·笔记·测试开发·学习·flask
幸运超级加倍~3 小时前
软件设计师-上午题-16 算法(4-5分)
笔记·算法
王俊山IT4 小时前
C++学习笔记----10、模块、头文件及各种主题(一)---- 模块(5)
开发语言·c++·笔记·学习
Yawesh_best5 小时前
思源笔记轻松连接本地Ollama大语言模型,开启AI写作新体验!
笔记·语言模型·ai写作
CXDNW6 小时前
【网络面试篇】HTTP(2)(笔记)——http、https、http1.1、http2.0
网络·笔记·http·面试·https·http2.0