编写一个猜数游戏
猜数游戏,它会首先生成一个1到100之间的随机整数,并紧接着请求玩家对这个数字进行猜测。假如玩家输入的数字与随机数不同,那么程序将给出数字偏大或偏小的提示。而假如玩家猜中了我们准备的数字,那么程序就会打印出一段祝贺信息并随之退出。
创建一个新的项目
使用Cargo来开始一个新的项目:
1 | $ cargo new guessing_game |
处理一次猜测
猜数游戏的第一部分会请求用户进行输入,并检查该输入是否满足预期的格式。
src/main.rs
1 | use std::io; |
接下来看看上面的代码做了什么?
为了获得用户的输入并将其打印出来,需要把标准库(也就是所谓的std)中的io
模块引入当前的作用域中:
1 | use std::io; |
作为默认行为,Rust会将预导入(prelude)模块内的条目自动引入每一段程序的作用域中,它包含了一小部分相当常用的类型。但假如你需要的类型不在预导入模块内,那么我们就必须使用use语句来显式地进行导入声明。std::io
库包含了许多有用的功能,可以使用它来获得用户的输入数据。
main
函数是一段程序开始的地方:
1 | fn main() { |
上面的fn
语法声明了一个新的函数,而紧随名称后的圆括号()
则意味着当前函数没有任何参数,最后的花括号{
被用来标识函数体的开始。
println!
宏,它被用来将字符串打印到屏幕上:
1 | println!("猜数游戏!"); |
使用变量存储值
接下来,创建了一个存储用户输入数据的地方:
1 | let mut guess = String::new(); |
这个以let
开头的语句创建了一个新的变量(variable)。在Rust中,变量都是默认不可变的,使用mut
关键字来声明一个变量是可变的:
1 | let foo = 5; // foo是不可变的 |
上面let mut guess
语句会创建出一个名为guess
的可变变量了。在这行语句中,等号(=)
的右边是guess
被绑定的值,也就是调用函数String::new
后返回的结果:一个新的String实例。String是标准库中的一个字符串类型,它在内部使用了UTF-8格式的编码并可以按照需求扩展自己的大小。
String::new
中的::
语法表明new
是String
类型的一个关联函数(associated function)
。关联函数在某些语言中也被称为静态方法(static method)
。
这个new函数会创建一个新的空白字符串。你会在许多类型上发现new函数,因为这是创建类型实例的惯用函数名称。
为了引入标准库中的输入/输出功能,在程序的第一行使用了语句use std::io
。现在将调用io模块中的关联函数stdin
:
1 | io::stdin().read_line(&mut guess) |
stdin
函数会返回类型std::io::Stdin
的实例,它被用作句柄来处理终端中的标准输入。这行代码随后的部分,.read_line(&mut guess)
,调用了标准输入句柄的read_line
方法来获得用户输入。另外,read_line
还在调用的过程中使用了一个参数:&mut guess
。
由于read_line方法会将当前用户输入的数据不加区分地存储在字符串中,所以它需要接收一个传入的字符串作为参数。传入的变量还需要是可变的,因为这一方法会在记录用户输入的过程中修改字符串。
参数前面的&
意味着当前的参数是一个引用。你的代码可以通过引用在不同的地方访问同一份数据,而无须付出多余的拷贝开销。引用与变量一样,默认情况下也是不可变的。因此,需要使用&mut guess
而不是&guess
来声明一个可变引用。
使用Result类型来处理可能失败的情况
1 | .expect("Failed to read line"); |
read_line会将用户输入的内容存储到我们传入的字符串中,但与此同时,它还会返回一个io::Result
值。在Rust标准库中,可以找到许多以Result命名的类型,它们通常是各个子模块中Result泛型的特定版本,比如这里的io::Result
。Result是一个枚举类型。枚举类型由一系列固定的值组合而成,这些值被称作枚举的变体。
对于Result而言,它拥有Ok
和Err
两个变体。其中的Ok
变体表明当前的操作执行成功,并附带代码产生的结果值。相应地,Err
变体则表明当前的操作执行失败,并附带引发失败的具体原因。
通过println! 中的占位符输出对应的值
最后一行代码:
1 | println!("你猜的数字是: {guess}"); |
将存储的用户输入打印出来。这段宏调用的第一个参数是用于格式化的字符串,而字符串中的那对花括号{}
则是一个占位符,它用于将后面的参数值插入自己预留的特定位置。也可以使用花括号来同时打印多个值:第一对花括号对应格式化字符串后的第一个参数,第二对花括号对应格式化字符串后的第二个参数,以此类推。下面的代码展示了如何调用println!
来同时打印多个值:
1 | let x = 5; |
生成一个幸运数字
下一步,需要生成一个幸运数字来供玩家进行猜测。为了保证一定的可玩性,并使每局游戏都有不同的体验,这个生成的幸运数字将会是随机的。为了让游戏不会过分困难,这个随机数字被限制在1到100之间。Rust团队并没有把类似的随机数字生成功能内置到标准库中,而是选择将它作为rand包(rand crate)提供给用户。
借助包来获得更多功能
Cargo最主要的功能就是帮助我们管理和使用第三方库。在使用rand编写代码之前,我们需要修改Cargo.toml文件来将rand包声明为依赖。现在让我们打开文件,并在Cargo生成的[dependencies]
区域下方添加依赖:
1 | [dependencies] |
运行cargo build
时,Cargo就会自动更新注册表中所有可用包的最新版本信息,并根据指定的新版本来重新评估你对rand的需求。
生成一个随机数
1 | use std::io; |
首先,额外增加了一行use语句:use rand::Rng
。这里的Rng
是一个trait
(特征),它定义了随机数生成器需要实现的方法集合。为了使用这些方法,我们需要显式地将它引入当前的作用域中。
另外,我们还在中间新增了两行代码。第一行中的函数rand::thread_rng
会返回一个特定的随机数生成器:它位于本地线程空间,并通过操作系统获得随机数种子。随后,我们调用了这个随机数生成器的方法gen_range
。这个方法是在刚刚引入作用域的Rng rait中定义的,它接收两个数字作为参数,并生成一个范围在两者之间的随机数。值得指出的是,它的随机数空间包含下限但不包含上限,所以我们可以指定1和101来获得1到100之间的随机整数。
比较猜测数字和幸运数字
src/main.rs
1 | use std::io; |
当我们尝试编译上面的代码,会报如下错误:
1 | | |
该错误的核心在于示例中的代码存在不匹配的类型。Rust有一个静态强类型系统,同时,它还拥有自动进行类型推导的能力。当我们编写let guess = String::new()
时,虽然我们没有做出任何显式的声明,但Rust会自动将变量guess的类型推导为String
。另一方面,lucky_number
是一个数值类型。有许多数值类型可以包含从1到100之间的整数,比如i32(32位整数)、u32(32位无符号整数)、i64(64位整数)等。除非我们在代码中增加更多用于推导类型的信息,否则Rust会默认将lucky_number
视作i32
类型。总而言之,编译器指出的错误就是:Rust无法将字符串类型(String类型)和数值类型直接进行对比。
为了正常进行比较操作,我们需要将程序中读取的输入从String类型转换为数值类型。这一转换可以通过在main函数中增加两行代码来完成:
1 | use std::io; |
在这里创建了一个新的变量guess,不过等等,我们不是已经使用过这个名字了吗?没错,但Rust允许使用同名的新变量guess来隐藏(shadow)旧变量的值。这一特性通常被用在需要转换值类型的场景中,它在本例中允许我们重用guess这个变量名,而无须强行创造出guess_str之类的不同的名字。
字符串的parse
方法会尝试将当前的字符串解析为某种数值。由于这个方法可以处理不同的数值类型,所以我们需要通过语句let guesss: u32
来显式地声明我们需要的数值类型。guess
后面的冒号(:)告诉Rust我们将手动指定当前变量的类型。而这里的u32
则是一个32位无符号整型,它是Rust内置的数值类型之一。对于不大的正整数来说,u32已经完全可以满足需求了,值得指出的是,由于我们将guess手动标记为了u32,并且将它和lucky_number进行了比较,所以Rust会将lucky_number也推导为相同的u32类型。
现在让我们运行程序试试:
1 | D:\Github\rust\guessing_game\src>cargo run |
这个游戏已经大体成型了,但玩家只能做出一次猜测,这显然是不够的。接下来,我们会加入一个循环来完善这个游戏。
使用循环来实现多次猜测
在Rust中,loop
关键字会创建一个无限循环。我们可以将它加入当前的程序中,进而允许玩家反复地进行猜测抉择:
1 | use rand::Rng; |
在猜测成功时优雅地退出
现在让我们给程序增加一条break语句,使得玩家在猜对数字后能够正常退出游戏。
1 | use rand::Rng; |
处理非法输入
为了进一步改善游戏的可玩性,我们可以在用户输入了一个非数字数据时简单地忽略这次猜测行为,并使用户可以继续进行猜测,从而避免程序发生崩溃。
1 | use rand::Rng; |
最终的完整代码:
1 | use rand::Rng; |
运行结果:
1 | 猜数游戏! |