Rust 2.14 猜谜
11 Jan 2015 by LelouchHe
好的!我们学习完了Rust的基础知识.现在来写一个大点的程序.
对于我们第一个项目,我们实现一个初学者的编程问题:猜谜游戏.是这样的:我们的程序生成一个1到100的随机数,然后让我们猜,能根据我们的猜测判断是太大还是太小,一旦我们猜对,就恭喜我们赢了.还行吧?
准备
先准备下环境.回到项目目录.还记得我们如何为hello_world
创建目录和Cargo.toml
么?Cargo有一个专门的命令来做这个.如下:
$ cd ~/projects
$ cargo new guessing_name --bin
$ cd guessing_name
我们把项目名称作为参数传给cargo new
,然后加上--bin
,因为我们想要一个可执行文件,而不是库.
看下生成的Cargo.toml
:
[package]
name = "guessing_game"
version = "0.0.1"
authors = [ "Your name <you@example.com>" ]
Cargo会从环境变量中获取相关信息.要是不对的话,直接修改即可.
最后,Cargo还创建了一个”Hello, World!”.看下src/main.rs
:
fn main() {
println!("Hello, world!");
}
(译者: 我的这个版本貌似还会生成git配套的东西)
直接构建即可:
$ cargo build
(译者: 不重要的错误/输出我就忽略了)
搞定!再打开src/main.rs
.我们会在这里写代码.后面我们会学习如果处理多文件项目.
在我们继续之前,给你展示另一个Cargo命令:run
.cargo run
类似cargo build
,但构建之后,会运行生成的可执行文件.可以试下:
$ cargo run
Hello, world!
赞!当我们快速迭代时,非常有用.这个项目就是如此,我们需要这种快速迭代的功能.
开发游戏
接下来开始开发!第一件事情是让我们的玩家可以进行输入.在src/main.rs
输入如下:
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let input = io::stdin().read_line()
.ok()
.expect("Failed to read line");
println!("You guessed: {}", input);
}
在讨论标准输入时,见过类似的代码.我们使用use
引入std::io
模块,然后在main
函数中实现逻辑.我们输出游戏的标识,让用户进行猜谜,获取其输入,最后打印出来.
因为我们在标准IO中讨论过这部分,在此就不会细说了.要是你不太熟悉,最好重新学习下那部分内容.
生成谜底
接下来,我们要生成谜底.为了能够生成,我们需要使用还没介绍过的随机数生成器.Rust在标准库中包含很多有用的函数.要是你需要某些功能,很有可能已经在标准库里了.此处,我们确实知道Rust有随机数生成器,但还不知道怎么用.
查询文档.Rust有一个页面专门描述标准库.你可以在这里看到.页面里有很多信息,但最赞的地方是那个搜索框.在页面最上方,你可以输入搜索词.现在搜索功能还比较基础,但会越来越好的.如果你输入”random”,会看到这个页面.第一个链接就指向std::rand::random
.点击链接,就进入了它的文档页了.
这个页面展示了一些信息:函数的原型,解释说明和使用示例.让我们把random
加入到刚才的程序,看看运行结果如何:
use std::io;
use std::rand;
fn main() {
println!("Guess the number!");
let secret_number = (rand::random() % 100) + 1;
println!("The secret number is: {}", secret_num);
println!("Please input your guess.");
let input = io::stdin().read_line()
.ok()
.expect("Failed to read line");
println!("You guessed: {}", input);
}
我们首先如文档所述,添加了use std::rand
.然后增加了let
语句创建了secret_number
的变量,再打印其值.
你也许会好奇为什么要对rand::random()
的结果使用%
.这是取模运算符(modulo),返回的是除法的余数.对rand::random()
结果取模,我们就让返回值在0到99之间.然后加1,其范围就在1到100之间了.取模会带来一定的偏差,但对此处影响不大(译者: 指的应该是取模对随机数的结果有一定影响,不过反正也是伪随机,区别不大).
使用cargo build
进行编译:
$ cargo build
error: the type of this value must be known in this context
let secret_number = (rand::random() % 100) + 1;
^~~~~~~~~~~~~~
无法编译!Rust指出”该值的类型必须在此处已知”.这是什么意思?其实,rand::random()
可以生成很多种类型的随机值,而不仅仅是整型.此处,Rust不知道应该生成哪种类型的值.所以我们需要给予提示.对于数字字面值,我们可以在后面加上i32
的后缀表示这是整型,但函数是不能这样使用的.对函数有个特殊的语法,是这样的:
rand::random::<i32>();
意思是”返回一个i32
类型的随机数”.我们可以修改下原来的代码:
use std::io;
use std::rand;
fn main() {
println!("Guess the number!");
let secret_number = (rand::random::<i32>() % 100) + 1;
println!("The secret number is: {}", secret_num);
println!("Please input your guess.");
let input = io::stdin().read_line()
.ok()
.expect("Failed to read line");
println!("You guessed: {}", input);
}
尝试运行几次该程序:
$ cargo run
Guess the number!
The secret_number is: -29
Please input your guess.
42
You guessed: 42
等等.-29?我们要的是1到100之间的数.我们有2个可以改进的地方:要不然让random()
生成一个无符号随机数,要不然使用abs()
取其绝对值.此处我们让它生成无符号随机数.如果想要一个随机的正数,我们需要让random()
生成一个正数.现在代码如下:
use std::io;
use std::rand;
fn main() {
println!("Guess the number!");
let secret_number = (rand::random::<usize>() % 100) + 1;
println!("The secret number is: {}", secret_num);
println!("Please input your guess.");
let input = io::stdin().read_line()
.ok()
.expect("Failed to read line");
println!("You guessed: {}", input);
}
(译者: uint
类型已经废弃,需要使用usize
)
尝试运行下:
$ cargo run
赞!接下来,让我们比较大小
比较大小
如果你还记得,我们之前实现过一个比较大小的cmp
函数.我们把那个加过来,再加上match
来匹配其返回结果:
use std::io;
use std::rand;
use std::cmp::Ordering;
fn main() {
println!("Guess the number!");
let secret_number = (rand::random::<usize>() % 100) + 1;
println!("The secret number is: {}", secret_num);
println!("Please input your guess.");
let input = io::stdin().read_line()
.ok()
.expect("Failed to read line");
println!("You guessed: {}", input);
match cmp(input, secret_number) {
Ordering::Less => println!("Too small"),
Ordering::Greater => println!("Too big"),
Ordering::Equal => println!("You win!"),
}
}
fn cmp(a: i32, b: i32) -> Ordering {
if a < b { Ordering::Less }
else if a > b { Ordering::Greater }
else { Ordering::Equal }
}
超时构建下,会得到如下错误:
$cargo build
error: mismatched types: expected `i32` but found `String`
match cmp(input, secret_number)
^~~~~
error: mismatched types: expected `i32` but found `usize`
match cmp(input, secret_number)
^~~~~~~~~~~~~
这个错误开发Rust程序时经常遇见,这也是Rust最厉害的地方.写一些代码,看看它能否编译,Rust会告诉你哪些地方做错了.这里,cmp
函数的参数是整型,但我们给了它无符号整型(译者: 指的是第二个错误).这个很容易修复,因为cmp
是我们写的!修改参数类型如下:
use std::io;
use std::rand;
use std::cmp::Ordering;
fn main() {
println!("Guess the number!");
let secret_number = (rand::random::<usize>() % 100) + 1;
println!("The secret number is: {}", secret_num);
println!("Please input your guess.");
let input = io::stdin().read_line()
.ok()
.expect("Failed to read line");
println!("You guessed: {}", input);
match cmp(input, secret_number) {
Ordering::Less => println!("Too small"),
Ordering::Greater => println!("Too big"),
Ordering::Equal => println!("You win!"),
}
}
fn cmp(a: usize, b: usize) -> Ordering {
if a < b { Ordering::Less }
else if a > b { Ordering::Greater }
else { Ordering::Equal }
}
再编译下:
$cargo build
error: mismatched types: expected `i32` but found `String`
match cmp(input, secret_number)
^~~~~
这个错误和上一个错误类似:我们希望类型是usize
,但最后得到的却是String
类型.这是因为input
是从标准输入来的,你可以输入任意东西.比如:
糟糕!另外,你注意到了,我们的程序编译没过,但还能运行.这是因为上次编译出的程序还在.别搞混了.
无论如何,我们有一个String
,但却需要usize
.怎么办?有一个函数恰好能完成这个转换:
let input = io::stdin().read_line()
.ok()
.expect("Failed to read line");
let input_number: Option<usize> = input.parse();
parse
函数将&str
转变为其他类型.我们使用类型提示规定好它返回的类型.还记得random()
的类型提示么?是这样的:
rand::random::<usize>()
也可以这样:
let x: usize = rand::random();
这个情况下,我们明确指示x
是usize
类型,Rust就能正确的指示random()
的返回类型.类似的,下面2种方式都是可行的:
let input_num = "5".parse::<usize>();
let input_num: Option<usize> = "5".parse();
不论怎样,我们都能得到一个数字.现在代码成了下面这样:
use std::io;
use std::rand;
use std::cmp::Ordering;
fn main() {
println!("Guess the number!");
let secret_number = (rand::random::<usize>() % 100) + 1;
println!("The secret number is: {}", secret_num);
println!("Please input your guess.");
let input = io::stdin().read_line()
.ok()
.expect("Failed to read line");
let input_num: Option<usize> = input.parse();
println!("You guessed: {}", input_num);
match cmp(input_num, secret_number) {
Ordering::Less => println!("Too small"),
Ordering::Greater => println!("Too big"),
Ordering::Equal => println!("You win!"),
}
}
fn cmp(a: usize, b: usize) -> Ordering {
if a < b { Ordering::Less }
else if a > b { Ordering::Greater }
else { Ordering::Equal }
}
尝试构建下:
$ cargo build
error: mismatched types: expected `usize` but found `Option<usize>`
match cmp(input_num, secret_number)
^~~~~~~~~
额!input_num
的类型是Option<usize>
,而不是usize
.我们要对其进行解包(unwrap).如果你还记得的话,match
可以完成这个操作.如下:
use std::io;
use std::rand;
use std::cmp::Ordering;
fn main() {
println!("Guess the number!");
let secret_number = (rand::random::<usize>() % 100) + 1;
println!("The secret number is: {}", secret_num);
println!("Please input your guess.");
let input = io::stdin().read_line()
.ok()
.expect("Failed to read line");
let input_num: Option<usize> = input.parse();
let num = match input_num {
Some(num) => num,
None => {
println!("Please input a number!");
return;
}
};
println!("You guessed: {}", num);
match cmp(num, secret_number) {
Ordering::Less => println!("Too small"),
Ordering::Greater => println!("Too big"),
Ordering::Equal => println!("You win!"),
}
}
fn cmp(a: usize, b: usize) -> Ordering {
if a < b { Ordering::Less }
else if a > b { Ordering::Greater }
else { Ordering::Equal }
}
使用match
,可以将Option
中的usize
值取出,或者就直接打印错误信息并推出.我们试下:
$ cargo run
Guess the number!
The secret_number is: 17
Please input your guess.
5
Please input a number!
额?怎么会!
其实上确实如此.从stdin()
获取的行输入,会得到整行输入,包括摁下”Enter”时的’\n’.所以,parse
看到的是字符串”5\n”,然后表示”额,这个不是数字欸,里面根本没有数字啊”.幸运的是,&str
有一个简单的方法处理这个: trim()
.做个小修改,如下:
use std::io;
use std::rand;
use std::cmp::Ordering;
fn main() {
println!("Guess the number!");
let secret_number = (rand::random::<usize>() % 100) + 1;
println!("The secret number is: {}", secret_num);
println!("Please input your guess.");
let input = io::stdin().read_line()
.ok()
.expect("Failed to read line");
let input_num: Option<usize> = input.trim().parse();
let num = match input_num {
Some(num) => num,
None => {
println!("Please input a number!");
return;
}
};
println!("You guessed: {}", num);
match cmp(num, secret_number) {
Ordering::Less => println!("Too small"),
Ordering::Greater => println!("Too big"),
Ordering::Equal => println!("You win!"),
}
}
fn cmp(a: usize, b: usize) -> Ordering {
if a < b { Ordering::Less }
else if a > b { Ordering::Greater }
else { Ordering::Equal }
}
运行下:
$ cargo run
赞!你看,就算我在输入里加了很多空格,它还是能得到我输入的数字.多运行下该程序,验证下猜数的功能是正常的.
Rust编译器帮了我们很大忙.这种技术称为”编译器依赖”(leaning on the compiler),对我们编写代码很有用.让编译错误信息指导我们如何正确编码.
现在,主要功能已经完备了,唯一要做的就是进行不停猜测了.添加以下循环功能.
循环
前面介绍过,loop
表示无限循环.代码如下:
use std::io;
use std::rand;
use std::cmp::Ordering;
fn main() {
println!("Guess the number!");
let secret_number = (rand::random::<usize>() % 100) + 1;
println!("The secret number is: {}", secret_num);
loop {
println!("Please input your guess.");
let input = io::stdin().read_line()
.ok()
.expect("Failed to read line");
let input_num: Option<usize> = input.trim().parse();
let num = match input_num {
Some(num) => num,
None => {
println!("Please input a number!");
return;
}
};
println!("You guessed: {}", num);
match cmp(num, secret_number) {
Ordering::Less => println!("Too small"),
Ordering::Greater => println!("Too big"),
Ordering::Equal => println!("You win!"),
}
}
}
fn cmp(a: usize, b: usize) -> Ordering {
if a < b { Ordering::Less }
else if a > b { Ordering::Greater }
else { Ordering::Equal }
}
可以运行下.但等等,我们刚刚添加了无限循环?是的.还记得return
么?要是输错了数字,就会return
退出程序了.看下:
$ cargo run
额!输成非数字确实退出了.但其他情况却不理想.首先,赢了游戏之后也应该退出:
use std::io;
use std::rand;
use std::cmp::Ordering;
fn main() {
println!("Guess the number!");
let secret_number = (rand::random::<usize>() % 100) + 1;
println!("The secret number is: {}", secret_num);
loop {
println!("Please input your guess.");
let input = io::stdin().read_line()
.ok()
.expect("Failed to read line");
let input_num: Option<usize> = input.trim().parse();
let num = match input_num {
Some(num) => num,
None => {
println!("Please input a number!");
return;
}
};
println!("You guessed: {}", num);
match cmp(num, secret_number) {
Ordering::Less => println!("Too small"),
Ordering::Greater => println!("Too big"),
Ordering::Equal => {
println!("You win!");
return;
}
}
}
}
fn cmp(a: usize, b: usize) -> Ordering {
if a < b { Ordering::Less }
else if a > b { Ordering::Greater }
else { Ordering::Equal }
}
在You win!
之后添加return
,这样赢了之后就能退出了.还有一些要修复:输入了非数字之后,不要退出,而是忽略它.我们把return
改成continue
:
use std::io;
use std::rand;
use std::cmp::Ordering;
fn main() {
println!("Guess the number!");
let secret_number = (rand::random::<usize>() % 100) + 1;
println!("The secret number is: {}", secret_num);
loop {
println!("Please input your guess.");
let input = io::stdin().read_line()
.ok()
.expect("Failed to read line");
let input_num: Option<usize> = input.trim().parse();
let num = match input_num {
Some(num) => num,
None => {
println!("Please input a number!");
continue;
}
};
println!("You guessed: {}", num);
match cmp(num, secret_number) {
Ordering::Less => println!("Too small"),
Ordering::Greater => println!("Too big"),
Ordering::Equal => {
println!("You win!");
return;
}
}
}
}
fn cmp(a: usize, b: usize) -> Ordering {
if a < b { Ordering::Less }
else if a > b { Ordering::Greater }
else { Ordering::Equal }
}
好了,我们运行下:
$ cargo run
赞!修复最后一下,就完成这个游戏了.你能想到是什么么?对了,我们不想把谜底打出来.这个用来测试比较方便,但游戏里加上这个就不好了.下面是最终的代码:
use std::io;
use std::rand;
use std::cmp::Ordering;
fn main() {
println!("Guess the number!");
let secret_number = (rand::random::<usize>() % 100) + 1;
loop {
println!("Please input your guess.");
let input = io::stdin().read_line()
.ok()
.expect("Failed to read line");
let input_num: Option<usize> = input.trim().parse();
let num = match input_num {
Some(num) => num,
None => {
println!("Please input a number!");
continue;
}
};
println!("You guessed: {}", num);
match cmp(num, secret_number) {
Ordering::Less => println!("Too small"),
Ordering::Greater => println!("Too big"),
Ordering::Equal => {
println!("You win!");
return;
}
}
}
}
fn cmp(a: usize, b: usize) -> Ordering {
if a < b { Ordering::Less }
else if a > b { Ordering::Greater }
else { Ordering::Equal }
}
搞定
这里,你就成功完成了猜谜游戏!恭喜!
你已经学会基本的Rust语法.这和你以前使用的语言是很类似的.这些基础语法和语义是你继续学习Rust的基础.
现在,你的基础已经很不错了,是时候学习其他更复杂的Rust功能了.