猜数游戏
纸上得来终觉浅,绝知此事要躬行。实践才能出真知,因此本文内容将通过一个小项目快速帮我们上手Rust语言。其中可能会出现一些目前还不是很了解的知识,但没事,后续通过学习我们会慢慢了解的,现在我们先体会一下Rust的运用。
我们会实现一个经典的新手编程问题:猜数字游戏。游戏的规则如下:程序将会生成一个 1 到 100 之间的随机整数。然后提示玩家输入一个猜测值。输入后,程序会指示该猜测是太低还是太高。如果猜对了,游戏会打印祝贺信息并退出。
第一步——读取用户输入并输出
我们使用cargo new guessing_game
命令新建一个名为guessing_game
的项目。
与上一篇文章类似,我们首先可以编写对用户输入的读取,并进行输出的功能代码:
use std::io;fn main() {println!("Guess the number!");println!("Please input your guess.");let mut guess = String::new();io::stdin().read_line(&mut guess).expect("Failed to read line");println!("You guessed: {}", guess);
}
第6行代码表示创建了一个变量,其中mut
表示该变量可变,若不加该关键字表示变量不可变。String
是标准库提供的字符串类型,它属于prelude,因此无需显式引入。双冒号::
表明new
是String
的一个关联函数,通过new
函数可以创建一个新的空字符串。
第7行中,io
库中的stdin()
可以使程序能够处理用户的输入。
第8行中,read_line()
函数先接收了guess
变量的引用,&
就表示“引用”,它允许多处代码访问同一个数据,而无需在内存中多次拷贝。经过函数处理后会返回一个 Result 类型的结果,这个 Result 是一种枚举类型,枚举类型变量的值可能有多种可能的状态,我们把每种可能的状态称为一种枚举成员
。Result 类型的枚举成员有Ok
和Err
两种,前者表示“操作成功”,并附带成功时产生的值;后者表示“操作失败”,且同样附带了操作失败的原因等信息。
第9行的expect()
函数会接收 Result 类型的结果,如果是 Ok
,则返回其值;如果是Err
,则返回相应的内容。如果此处不加该函数去处理错误,Rust语言将不通过编译,这也能体现出Rust语言的安全性。
编写完第一部分的代码之后,运行一下结果:
顺利读取输入并返回结果!那现在开始第二部分。
第二步——生成随机数字
接下来需要实现随机数字的生成功能,这时候我们需要引入外部crate
帮助我们实现这一功能。
此处我们要引入的是rand
这个包,引入外部包有两种方法:
- 通过
Cargo.toml
文件进行添加:
此处表示引入 0.8.5 版本的rand
包。
- 通过在命令行输入
cargo add [包名]@[版本号]
来添加外部包。
两个方法的效果均相同,并且在构建代码之后会生成一个Cargo.lock
文件,这个文件可以保证任何人在构建这个代码时,只会使用指定的包版本,除非我们显式地升级版本。这有助于程序的“可重复构建”,因此它通常会和项目中地其余代码一样纳入到版本控制系统中。
引入包之后,我们就要利用它实现生成随机数字功能了。具体实现代码如下:
use rand::Rng;fn main(){println!("Guess the number!");let secret_number = rand::thread_rng().gen_range(1..101);......
}
在代码中,我们显式引入了rand::Rng
,其中Rng
是一个trait
,它定义了随机数生成器应该实现的方法,类似 Java 中的接口。关于 trait
,后续经过学习后会进行相应的补充。
在第5行,我们定义了一个变量(注意:这个变量是不可变的),通过rand::thread_rng()
函数提供实际使用的随机数生成器,它位于当前执行线程的本地环境中,并从操作系统获取seed。接着调用随机数生成器的gen_range
方法获取随机数。在gen_range
方法中,我们使用了一个范围表达式作为参数来生成此范围内的随机数。范围表达式分为x..y
和x..=y
两种,前者表示[x,y)
,后者表示[x,y]
。
多执行几次代码,可以看到随机数可以成功生成:
第三步——比较
现在有了用户输入和随机数,那么就可以比较二者实现猜数功能了。在这个步骤中将引入另一个外部包std::cmp::Ordering
,Ordering
也是一个枚举类型,它有三个枚举成员:Less
、Greater
、Equal
。我们会使用match
表达式,并利用cmp
方法来比较用户输入和随机数,代码如下:
use std::cmp::Ordering;fn main(){......let secret_number = rand::thread_rng().gen_range(1..101);println!("Please input your guess.");let mut guess = String::new();......match guess.cmp(&secret_number) { // 报错Ordering::Less => println!("Too small!"),Ordering::Greater => println!("Too big!"),Ordering::Equal => println!("You win!"),}
}
match
表达式和switch
语句非常类似,当符合某一个情况时,便会返回对应的内容。然而上述代码在编译阶段会报错,原因在于secret_number
和guess
这两个变量不是同一个数据类型,前者是整数类型,后者则是字符串。因此需要将二者的数据类型统一。
对此,可以添加如下代码:
fn main(){......let mut guess = String::new();......let guess: u32 = guess.trim().parse().expect("Please input number!");......
}
第4行中,我们定义了一个u32
类型的guess
变量,变量名后加冒号就代表显式声明变量的数据类型。相信有人也发现了,这里居然有两个同名变量。这也是Rust语言的特性,这种特性名为遮蔽(Shadowing)
,关于“遮蔽”的更多内容,在经过后面的学习会进行相应补充,此处只需知道这个特性常被用于将一个类型的值转换为另一个类型的值。
在第4行的等式右边,我们分别用到了trim()
、parse()
和expect()
方法。trim()
方法用于去除字符串前后多余的空白;parse()
用于将字符串转换成其他类型,因为前面已经告知了Rust,guess
变量的类型为u32
,因此经过parse()
方法之后会得到一个整数类型的用户输入。由于parse()
方法返回的是一个 Result 类型,因此需要一个方法处理错误。此处我们继续使用expect()
方法进行处理。
而且Rust确实很聪明,当你定义
guess
变量为u32
类型后,与其进行比较的secret_number
也自动被赋为u32
类型了
执行代码之后可以发现已经会反馈比较结果了,但是在猜了一次之后就停止了,这样体验感极差,因此需要让它保持循环,直到用户猜出数字。
第四步——引入循环进行多次猜测
loop
关键字可以实现一个无限循环,将其加入到代码中实现无限循环,来让用户有更多机会猜出数字:
......
fn main(){......loop {println!("Please input your guess.");......match guess.cmp(&secret_number) {Ordering::Less => println!("Too small!"),Ordering::Greater => println!("Too big!"),Ordering::Equal => println!("You win!"),}}
}
但这样面临一个问题,那就是无法跳出循环了。所以我们希望在用户猜对之后结束循环,因此上述代码将改为:
......
fn main(){......loop {println!("Please input your guess.");......match guess.cmp(&secret_number) {Ordering::Less => println!("Too small!"),Ordering::Greater => println!("Too big!"),Ordering::Equal => {println!("You win!");break;},}}
}
同时,我们也不希望用户输入非数字类型的输入时使程序崩溃,从而让用户可以继续猜测,因此可以进行下述优化:
......
fn main(){......loop {println!("Please input your guess.");......let guess: u32 = match guess.trim().parse(){Ok(num) => num,Err(_) => continue,};......}
}
利用match
表达式判断用户是否输入非数字类型输入,是的话输出数字,不是的话利用loop
循环重新要求输入。
整体代码
经过了不断优化,最终得到实现代码:
use std::cmp::Ordering;
use std::io;
use rand::Rng;fn main() {println!("Guess the number!");let secret_number = rand::thread_rng().gen_range(1..101);loop {println!("Please input your guess.");let mut guess = String::new();io::stdin().read_line(&mut guess).expect("Failed to read line");let guess: u32 = match guess.trim().parse(){Ok(num) => num,Err(_) => continue,}; println!("You guessed: {}", guess);match guess.cmp(&secret_number) {Ordering::Less => println!("Too small!"),Ordering::Greater => println!("Too big!"),Ordering::Equal => {println!("You win!");break;},}}
}
运行代码结果如图所示: