Home

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();

这个情况下,我们明确指示xusize类型,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功能了.