03-猜数游戏

前言

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

目标

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

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

关联知识

Crate

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

编写代码

创建项目

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

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

实现输入与输出

// 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,然后根据上面的指引使用:

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

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

3-猜数游戏"

生成随机数

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

// 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 库的的定义:

3-猜数游戏"

比较大小

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

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

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

上面的代码输出如下:

3-猜数游戏"

Rust 中的类型推论

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

3-猜数游戏"

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

3-猜数游戏"

那么,究竟什么原因,导致 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

3-猜数游戏"

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

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

就像上面的这个代码

3-猜数游戏"

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

结语

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

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

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

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

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

原文链接:https://juejin.cn/post/7327472360865087488 作者:kinertang

(0)
上一篇 2024年1月25日 上午10:53
下一篇 2024年1月25日 上午11:04

相关推荐

发表回复

登录后才能评论