前言
通过之前的学习,我们已经搭建好了 Rust 的环境,并且学会了如何使用 Cargo 这个构建与包管理工具,接下来,就通过一个实际的程序示例来正式学习一下 Rust 的一些常见的语法,如:let
、match
等方法的作用以及使用方式,以及外部的 crate
的相关介绍。
目标
我们要使用 Rust 实现一个猜数游戏,游戏的细节如下:
- 生成一个 1 ~ 100 的随机整数
- 命令行当中提示用户输入一个他猜测的数字
- 用户猜完之后,若用户猜的数与生成的随机数不一致,则程序提示用户猜测的数字太大了还是太小了。
- 如果用户猜测的数字等于生层的这个随机数,则提示猜对并终止程序。
关联知识
Crate
直译过来的意思是"箱",顾名思义,crate
在 Rust 当中就是代表的是:包 、库 、第三方依赖 。这个类比成我们熟悉的 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;
,这个库是一个枚举类型,有:Less
、Greater
、Equal
这三个枚举值,我们只需要按照上面代码的方式,对匹配到的每一个枚举值进行特异性处理即可。至于前面的 match
关键字,在 Rust
当中是一个非常重要且常见的关键字,我们以后在扩充学习一下。在这里,我们就只需要知道,match
会根据 guess.cmp
的结果枚举执行不同的操作即可。
上面的代码输出如下:
Rust 中的类型推论
或许有些细心的同学会发现,在我们还没有写 match
匹配方法时,我们上面的 secret_number
的类型是 i32
的,也就是 32 位的整数:
而当我们写了 match
相关的匹配语句时,上面的 secret_number
的类型竟然神奇地变成了 u32
:
那么,究竟什么原因,导致 secret_number
类型反复横跳呢?其实原因在于我们的 gen_range(1..100)
方法产生的随机整数的类型其实不唯一,i32
、u32
、i64
都能够产生 1 ~ 100
的随机数,在我们还没有使用这个变量时,这个方法默认返回的类型是 i32
,但当我们在后面进行匹配时,因为我们将 guess
的类型显示声明是 u32
,因此触发了 Rust
的类型推论机制,Rust 认为,既然你这里需要个 u32
类型的数字进行比较,而我的 secret_number
可以是 i32
、u32
、i64
这三种类型中的一种,发现有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
方法的参数和返回值也会根据传入的参数动态地改变了。由此也可以看出 Rust 和 Typescript 在设计上有很多异曲同工之妙。
结语
至此,我们已经使用 Rust 实现了一个简易版本的 猜数游戏,虽然这个游戏还有不少需要优化的点,比如:输入完无论正确还是错误程序就直接结束了,没办法一直猜测,直到最终正确。这个受限于文章的篇幅,以及本文涉及到的相关的知识点比较多,我们放在下一篇再继续优化完善。
在上面编写这个游戏的过程中,相信前端同学都会觉得,Rust 有很多跟 js/ts 很像的点,但有一些点又不一样。没错的,Rust 在设计层面上,可以说是博采众长,你总能发现他的语法里面的某些点,跟某些语言相似,这也是 Rust 的魅力之一。
我们通过本章的学习,了解了以下知识点:
- Crate 的基本概念
- 如何在 Rust 中引入第三方依赖包
- 如何接受用户的输入
- 如何生成一个随机数
- 如何进行类型转换
- Rust 中的类型推论
- 如何进行数值比对
可以说,这个程序是:"麻雀虽小五脏俱全"。带我们逐步深入的了解到了 Rust 的一些语法细节。