admin管理员组

文章数量:1646328

Programming a Guessing Game

  • Foreword 前言
  • Setting Up a New Project 建立一个新的项目
  • Processing a Guess 编写猜数字游戏
    • Storing Values with Variables 通过变量存储值
    • Handling Potential Failure with the `Result` Type 通过`Result`类型处理潜在的报错
    • Printing Values with println! Placeholders 通过 `println!` 占位符打印
    • Testing the First Part 测试第一版
  • Generating a Secret Number 产生一个秘密数字
    • Using a Crate to Get More Functionality 引用箱来获得更多的功能
    • Ensuring Reproducible Builds with the Cargo.lock File 通过Cargo.lock文件重构
    • Updating a Crate to Get a New Version 更新箱到一个新的版本
    • Generating a Random Number 产生一个随机数
  • Comparing the Guess to the Secret Number 比较随机数
  • Allowing Multiple Guesses with Looping 通过循环多次猜数字
    • Quitting After a Correct Guess 猜中后自动退出
    • Handling Invalid Input 处理异常输入
  • Summary 总结

Foreword 前言

这章让我们动动手,通过一个简单的项目来进一步了解Rust。本章会像你介绍一些Rust的基础概念并告诉你如何在一个真正的程序中使用它们。你将学习到letmatch, 方法调用,关联函数,外箱和其它一些知识。在后续的章节中我们会更加深入的了解这些知识,本章只是让你熟悉下基础。

我们将实现一个非常经典的适合新手的程序:猜数字游戏。程序会从1到100中随机选出一个数字并让玩家输入自己猜测的值。玩家输入完毕,程序会判断这个输入值与产生的随机数是否一致,究竟是偏大还是偏小。当玩家猜到的值与这个随机数一致时,游戏会显示祝贺信息并结束。

Setting Up a New Project 建立一个新的项目

就像第一章教给你的那样,将工作区切换到 projects 目录,使用Cargo创建一个新的项目 guessing_game ,接着进入到Cargo生成的 guessing_game 目录:

$ cargo new guessing_game
$ cd guessing_game

看下生成的 Cargo.toml 文件:

[package]
name = "guessing_game"
version = "0.1.0"
authors = ["Your Name <you@example>"]
edition = "2018"

[dependencies]

如果Cargo从你的环境变量中获得的作者信息不对,你可以修改它并保存。
正如第一章的介绍, cargo new 还会为你自动创建一个 Hello, world! 程序,检查下你的 src/main.rs 文件:

fn main() {
    println!("Hello, world!");
}

现在让我们通过 cargo run 命令来编译这个程序并运行它:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 1.50 secs
     Running `target/debug/guessing_game`
Hello, world!

使用 run 命令可以很便捷快速的来测试Rust程序,尤其是当你须要频繁修改一个项目的时候。本章的项目中,我们每一步操作前都须要通过 run 来进行测试。

现在请重新打开 src/main.rs 文件,我们将要开始往里写入代码了。

Processing a Guess 编写猜数字游戏

猜数字游戏的第一个部分是须要请求用户输入,并对用户的输入做处理,以确保用户输入的数据格式是我们期望的样子。

现在,我们先来让用户能够输入一个他猜测的数字:

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

这段代码包含了大量信息,下面我们来逐行解读。为了能捕获用户的输入并将结果输出到屏幕,我们须要将 io 这个标准输入输出库引入到作用域,io 来自Rust的标准库 std

use std::io;

缺省情况下,Rust在prelude(序幕)中只会将相当少的库引入作用域,如果你想用的库恰好不在序幕内,你就必须通过 use 语句去引入它。std::io 这个库提供了大量有用的特性,其中就包含了接收用户输入的能力。

译者注:序幕是Rust在创建程序时会缺省自动引入的标准库的清单,你可以在上面的超链接中了解更多关于序幕的知识。

就像你在第一章看到的那样,main 函数是整个程序的入口点。

fn main() {

fn 这个语法定义了一个新的函数;() 为空说明没有任何输入参数; { 标记着函数主体的起始。

println!是一个用来将字符串显示在显示器上的宏:

println!("Guess the number!");

println!("Please input your guess.");

这些代码介绍了游戏的内容并催促玩家输入。

Storing Values with Variables 通过变量存储值

接着我们开辟了一个空间用来存储用户的输入,就像这样:

let mut guess = String::new();

这个程序现在就有点意思了,这短短一行其实包含了很多内容。这是一个let 语句,用来创建一个 variable(变量)。不过,这里有另外一个例子:

let foo = bar;

在上面的程序中也创建了一个新的变量并将它绑定到了 bar变量的值上。在Rust中,变量默认是无法被修改的,我们会在第三章的"变量与可修改性"里对这个概念做详细的介绍。现在你只需要知道,只要在变量名前加上 mut,就能让它变成一个可以修改的变量,就像下面的例子:

let foo = 5; // 不可修改
let mut bar = 5; // 可修改

// 语法是用来提供注释信息的,它直到行尾结束。Rust将忽略被注释的内容,我们将在第三章中再对注释做详细介绍。

让我们回到猜数字程序。你现在知道了 let mut guess 创建了一个可修改的变量 guess。在 = 的另一边,是 guess 绑定的值,这个值是 String::new 的返回值,String::new 会返回一个 String 实例。String 是标准库的字符串类型,UTF-8编码且可以扩展长度。

::new中的::符号,用于指明这里的new是一个String类型的associated function(关联函数)。关联函数是指那些在类型上实现,在本例中是String,而非在String实例上实现的函数。一些其它的语言中,习惯称其为static method(静态方法)。

new函数创建了一个新的、空的字符串。很多类型都有new函数,这是个约定俗成的名字,用来代表一个可以产生某些类型值的函数。

总结下,let mut guess = String::new(); 创建了一个可修改变量并且当前它被绑定到了一个新的空的String实例。

回想起我们一开始已在程序开头通过use std::io;引入了标准库的输入输出功能,我们现在就能调用了io模块的stdin函数了:

io::stdin().read_line(&mut guess)
    .expect("Failed to read line");

如果在程序的开始部分没有使用 use std::io,这里我们就无法调用 std::io::stdin 这个函数。stdin 函数会返回一个 std::io::Stdin 的实例,这个实例是一个终端标准输入的句柄。

在代码的后半部分,.read_line(&mut guess) 调用了 read_line 这个方法来获取用户在终端中的输入,同时read_line用到了一个参数 &mut guess

read_line 的一个功能是将用户输入的标准输出放到一个字符串中,这个字符串就是它的参数。这个字符串参数必须要求是可修改的,只有这样 read_line 才能把用户的输入传给它。

& 这个标记说明参数是一个 reference(引用),通过使用引用,你可以直接访问可修改变量的数据而无须在内存中将它的值反复复制出来。引用是一个非常复杂的特性,Rust的一大优势就是能够更安全和容易的使用引用。在这个程序中,你不需要对引用了解太深,现在你只要知道变量和引用缺省情况下都是不允许修改的。因此你须要使用&mut guess来使得它可以修改。(第四章中将会详细解释有关引用的知识)

Handling Potential Failure with the Result Type 通过Result类型处理潜在的报错

目前为止,我们这行代码还未结束。上面的部分中只不过讨论了有关文本的内容,而剩下的部分则是这样一个方法:

.expect("Failed to read line");

当你准备通过类似 .foo()这样的语法来使用一个方法时,一个聪明的方法是另起一行,这样可以将长句子打散。我们当然可以这样写:

io::stdin().read_line(&mut guess).expect("Failed to read line");

然而太长的代码将不方便阅读,所以我们将它拆成了两行。让我们来讨论下第二行的内容。

上文已经提过,read_line将用户输入传入了我们定义的字符串中,但它还会返回一个值,一个io::Result对象。Rust的标准库中有许多类型名字叫Result,通常来说,每个标准库都会有它对应版本的一个子模块Result,譬如io::Result

Result类型是enumerations(枚举型),通常也可写作 enums 。枚举是中能包含多个值的集类型,枚举中包含的值通常也称为枚举变量。第六章中将再详细讨论枚举的内容。

对于Result,它的枚举变量是Ok或者ErrOk变量代表操作成功,Ok变量中包含了成功产生的值;Err变量代表操作失败,Err变量中包含了有关错误的相关说明和解释。

使用Result类型的目的是为了获取处理失败的信息。Result就像其它任何类型一样,都定义了一些方法。每个io::Result的实例都有一个expect method的方法可以调用。如果io::Result中的值是Errexpect方法就会让程序终止,并将你传给它的值作为错误消息在前端显示出来。read_line方法返回了Err,那通常来说应该是系统底层发生了错误。如果io::Result中的值是Ok,那么expect将会取出Ok中的值并将这个值传出来,这样你才能接着使用它。在我们这个例子中,这个值是用户在标准输入中输入的一串数字。

如果你不调用expect,程序可以编译,但你会得到一个警告信息:

$ cargo build
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
warning: unused `std::result::Result` which must be used
  --> src/main.rs:10:5
   |
10 |     io::stdin().read_line(&mut guess);
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: #[warn(unused_must_use)] on by default

Rust会警告你没有使用read_line返回的Result,说明这个程序无法处理一个可能的错误。

这时正确的做法是添加错误处理的逻辑,可因为你只想在错误发生时直接终止程序,所以你所要做的只是调用expect方法。在第九章中,我们还会学习如何从错误中恢复。

Printing Values with println! Placeholders 通过 println! 占位符打印

现在我们只有最后一行代码须要讨论了:

println!("You guessed: {}", guess);

这行代码会将我们保存的用户输入显示出来。在这段代码里,{}标识了一对占位符。你可以在文本中使用多个占位符,每个占位符在显示时会依次被后面的传入参数给填充。譬如下面一个println!的例子:

let x = 5;
let y = 10;

println!("x = {} and y = {}", x, y);

这段代码将在你的屏幕上显示 x = 5 and y = 10

Testing the First Part 测试第一版

让我们通过 cargo run 来测试我们猜数字游戏的第一版:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 2.53 secs
     Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
6
You guessed: 6

截止目前,我们已经完成了游戏的第一部分:我们获取到了键盘的输入值并将它打印了出来。

Generating a Secret Number 产生一个秘密数字

下一步,我们须要得到一个用户用来猜的秘密数字。秘密数字必须在每次游戏时都不同,这样才能有足够的乐趣来让你反复玩。让我们来产生一个1到100之间的随机数,好确保游戏不至于太难。Rust的标准库中并没有产生随机数的功能,但是Rust团队提供了一个rand箱。

Using a Crate to Get More Functionality 引用箱来获得更多的功能

请记住crate(箱)是指一系列Rust的源码文件。我们刚刚创建的项目是一个 binary crate(二进制箱),它是可执行的,而rand是一个library crate(库箱),它包含了一些可以被其它程序使用的代码。

Cargo对于外箱的使用是它的亮点之一。在我们准备在我们的代码中使用rand前,我们须要修改 Cargo.toml文件来把rand箱添加进我们的依赖项中。那么我们打开配置文件,修改最下面的[dependencies]段落:

[dependencies]

rand = "0.3.14"

Cargo.toml 文件中,每一个段落是以一个标题开始直到下一个标题出现。[dependencies]段落是用来让你告诉Cargo,你的项目依赖哪些外部的箱,并且这些箱具体哪个版本是你需要的。在本例中,我们指定了rand箱,且版本是0.3.14。Cargo能理解语义版本控制Semver,这是一种版本书写的标准。0.3.14实际上是^0.3.14的缩写,代表了“任何可以和版本0.3.14 API兼容的版本”。

现在,我们先不对代码做任何更改,直接build我们的项目,你会看到如下的信息:

$ cargo build
    Updating registry `https://github/rust-lang/crates.io-index`
 Downloading rand v0.3.14
 Downloading libc v0.2.14
   Compiling libc v0.2.14
   Compiling rand v0.3.14
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 2.53 secs

你或许会看到不同的版本号(幸亏了Semver规范,它们的代码是相互兼容的),也有可能展现顺序与上面略有不同。

现在我们的项目有了一个外部依赖项,Cargo从registry(注册信息)中获取到了这些依赖项的最新版本,我们这里的注册信息其实是Crates.io中数据的拷贝,Crates.io是Rust生态重要的一环,人们可以将他们的开源Rust项目在上面分享给其他人。

更新注册信息后,Cargo会检查[dependencies],并将你还没有的箱下载下来。本例中,尽管我们知识罗列了rand这一个依赖项,但Cargo还下载了libc,因为rand须要libc才能工作。箱下载完毕,Rust会编译它们,然后再连同依赖项一起编译我们的项目。

如果你不做任何修改,再次运行cargo build指令,除了Fnished这行外,你不会收到任何其它信息。因为Cargo知道所有依赖项已经下载并编译完毕,而且你也没有修改Cargo.toml中的任何东西。Cargo甚至还知道你没有修改你代码中的任何东西,所以它也不会编译你的项目,而是什么事也不做就直接退出。

如果你打开了 src/main.rs 文件,并做了一些微不足道的修改并保存,当你重新build项目时,你也只会看到两条信息:

$ cargo build
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 2.53 secs

这几行说明,当你修改了src/main.rs时,Cargo只会更新你自己的build。你的依赖项没有更改,Cargo意识到它们可以复用,因为Cargo已经一早将依赖项下载并编译好了。Cargo只会重建你代码那部分内容。

Ensuring Reproducible Builds with the Cargo.lock File 通过Cargo.lock文件重构

Cargo有一种机制来确保同样的项目,无论是你还是其他人重构时能得到一样的结果:Cargo只会使用你指定的依赖项版本,除非你特别指定了其它的依赖项版本。举个例子,如果下一个礼拜,rand的0.3.15版本发布,它包含了一个针对重要bug的修复但同时也有一个回退会导致你的代码中断,那这时我们重构项目,Cargo会怎么做呢?

上面问题的答案就是Cargo.lock这个文件,在你第一次使用cargo build命令时,它会自动在你的 guessing_game 目录下创建出来。每当项目第一次build,Cargo会解析所有符合标准的依赖项版本,并记录到 Cargo.lock 文件中。未来重构项目时,Cargo会检查是否存在Cargo.lock文件,如果存在,Cargo就会使用里面指定的版本而不是重新去解析新的版本。这样的机制可以能使你实现自动化的重构项目。换句话说,在我们这个例子中,只要你一直留着 Cargo.lock 文件,你的项目将始终使用 0.3.14 版本的 rand ,除非你明确指明要升级。

Updating a Crate to Get a New Version 更新箱到一个新的版本

如果你真的想更新一个箱,Cargo也提供了另外一个指令update,它会忽略 Cargo.lock 文件并解析符合你 Cargo.toml 要求的最新版本,并且执行完毕后,Cargo也会同步将箱的版本信息更新到 Cargo.lock 文件。

缺省情况下,Cargo只会去找版本大于 0.3.0 并小于 0.4.0 的版本。如果rand有两个激活的版本 0.3.150.4.0,当你执行cargo update时,会看到下面的信息:

$ cargo update
    Updating registry `https://github/rust-lang/crates.io-index`
    Updating rand v0.3.14 -> v0.3.15

同时你也意识到了你的 Cargo.lock 文件也有了个变化,rand 现在的版本变成了 0.3.15

如果你想使用 0.4.0 版本或者其它 0.4.x 系列的 rand,你必须要去更新你的 Cargo.toml 文件:

[dependencies]

rand = "0.4.0"

在你下次运行cargo build时,Cargo会更新箱的注册表,并获取你须要的新版本rand箱。

我们会在第十四章中聊聊更多有关Cargo
和Cargo生态的知识,现在你还不需要了解这么多。Cargo使得库的复用非常容易,Rustaceans通过一系列库的组合可以写出更为精简的代码。

Generating a Random Number 产生一个随机数

你已经将 rand 添加进了 Cargo.toml,让我们开始使用 rand,现在修改 src/main.rs 如下:

use std::io;
use rand::Rng;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1, 101);

    println!("The secret number is: {}", secret_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);
}

首先,我们在抬头加了行 use rand::RngRng trait(特性)定义了一系列随机数生成器实现的方法,所以我们必须把它引入到我们的作用域来。第十章会对于特性有更多的介绍。

接着我们在程序中间加了两行,rand::thread_rng 函数为我们提供了随机数生成器。这个生成器在本地线程中运行,并由操作系统提供随机数种子。然后我们调用这个随机数生成器的 gen_range 方法。这个方法在 Rng 特性中定义,并被我们用 use rand::Rng 添加进了作用域。gen_range 有两个参数,它产生的随机数将间于两个参数中间。因为这个方法是左闭右开的,所以如果我想获得一个1到100中间的随机数,那么参数必须指定为 1101

现在你可以不仅仅了解使用哪个特性、哪些方法和函数。每个箱的使用方法都在它自己的文档里。Cargo另一个牛逼的功能,你可以运行 cargo doc --open 指令,它找到你本地依赖项的文档,并在浏览器中打开来。如果你对于 rand 中其它的功能感兴趣,那么试试 cargo doc --open,之后在左边的侧边栏点击 rand

我们添加的第二行代码会将秘密数字显示出来。这可以方便我们在开发程序时做测试,但最终版本中我们须要删掉它。如果游戏在一开始就将答案显示出来,那也太不像样了。

试试运行几次我们的程序:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 2.53 secs
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 7
Please input your guess.
4
You guessed: 4
$ cargo run
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 83
Please input your guess.
5
You guessed: 5

你应该能够得到1到100之间的随机数,干得漂亮。

Comparing the Guess to the Secret Number 比较随机数

现在我们获得了用户的输入和一个随机数字,我们可以比较它们。看下下边的程序,它会编译失败,我们将解释它无法通过编译的原因:

use std::io;
use std::cmp::Ordering;
use rand::Rng;

fn main() {

    // ---snip---

    println!("You guessed: {}", guess);

    match guess.cmp(&secret_number) {
        Ordering::Less => println!("Too small!"),
        Ordering::Greater => println!("Too big!"),
        Ordering::Equal => println!("You win!"),
    }
}

这段代码中,我们又多了个 use 语句,它将标准库中的 std::cmp:Ordering 类型引入了我们程序的作用域。和 Result 一样,Ordering 也是枚举型,但是它的枚举变量是 LessGreaterEqual。当你比较两个值时,会返回这三个输出。

下面的5行代码,我们使用了 Ordering 类型。 cmp 方法用来比较两个值,当然它还可以用来比较任何可以比较的值。cmp通过 secret_number 变量的引用,比较了和guess的值。比较结果返回了一个 Ordering枚举变量,我们可以使用match表达式来获取这个 Ordering 枚举变量,并依据不同的枚举变量来决定下一步做什么。

match 表达式由 arm (手臂)组成。每一条手臂都包含了一个 pattern(模式)和一段代码,当传给 match 表达式的值符合某一条手臂的模式时,就会执行它相应的代码。Rust会将match获取到的值,依次查找相应的模式。match 结构和模式是Rust中相当强大的特性,它允许你罗列所有你代码将遇到的情况,并且Rust会确保你处理了所有可能性。这个特性我们会在第六章和第十八章中有详细介绍。

我们来通过一个例子看下match表达式在这里会发生什么。譬如用户猜了个50,而这次的随机数是38。等代码比对50和38时,cmp 方法会返回一个Ordering::Greater,因为50大于38。match表达式获取到了Ordering::Greate值,然后开始检查每条手臂的模式。第一条手臂的模式是Ordering::Less,不匹配,程序会跳过这条手臂的代码,开始查看下一臂。第二手臂的模式是Ordering::Greater,完全匹配!第二臂关联的代码被执行并在屏幕上打印出Too big!,同时match表达式结束,因为它已经不须要再查看最后一臂了。

然而,当我们尝试编译这段代码时,会发生如下报错信息:

$ cargo build
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
error[E0308]: mismatched types
  --> src/main.rs:23:21
   |
23 |     match guess.cmp(&secret_number) {
   |                     ^^^^^^^^^^^^^^ expected struct `std::string::String`, found integer
   |
   = note: expected type `&std::string::String`
   = note:    found type `&{integer}`

error: aborting due to previous error
Could not compile `guessing_game`.

错误提示的核心是这里存在mismatched types类型不匹配。Rust拥有一个强大的静态类型系统,然而它也有类型推断。当我们码下let mut guess = String::new() 时,Rust会推断这个 guess变量应该是一个String,所以它没有让我们写具体的类型。但另一方面, secret_number 是一个数字类型。对于一个1到100中的数字,有很多数字类型可以让我们使用:i32一个32位数字;u32一个32位无符号数字;i64一个64位数字… Rust缺省是使用i32。在我们不明确指定其它数字类型的情况下,secret_number就是一个32位数字。而报错的原因就是因为Rust无法比较一个字符串和数字。

最终,我们想要将程序获取的String输入转化为一个真正的数字,这样我们就能比较它和秘密数字。所以我们再次对main函数做了如下修改:

// --snip--

    let mut guess = String::new();

    io::stdin().read_line(&mut guess)
        .expect("Failed to read line");

    let guess: u32 = guess.trim().parse()
        .expect("Please type a number!");

    println!("You guessed: {}", guess);

    match guess.cmp(&secret_number) {
        Ordering::Less => println!("Too small!"),
        Ordering::Greater => println!("Too big!"),
        Ordering::Equal => println!("You win!"),
    }
}

可以看到,我们新增了如下两行:

let guess: u32 = guess.trim().parse()
    .expect("Please type a number!");

我们创建了一个变量guess。但是等等,程序不是已经有了一个叫guess的变量了么?是的,但是Rust允许我们 shadow(影子) 之前 guess ,给它一个新的值。shadow这个特性会经常在你须要将一个变量类型转化为另一个类型时使用。shadow让我们能够复用guess这个变量而不是强迫我们创建两个唯一的变量,像是guess_strguess。(第三章将更加详细的介绍shadow)

我们将 guess 绑定到了表达式 guess.trim().parse(),这个表达式中的guess还是指原来那个String对象。trim这个String实例的方法会将字符串前后的空格都给去掉。尽管u32只能包含数字字符串,但用户必须按回车才能触发read_line。当敲下回车时,换行符会被加到字符串中。距离来说,当用户输入5并按回车,guess的值会像是5\n\n代表换行符。而trim方法会剔除\n,使得结果变为5

字符串的parse方法可以把字符串转化为任意类型的数字,所以我们须要let guess: u32来明确告诉Rust我们想要转化的类型。guess 后面的 : 告诉Rust我们想要转化的变量类型。Rust有一些内置的数据类型:u32在是无符号的32位整数,对于一个小正数,它是一个不错的选择。你将在第三章中了解更多其它数字类型。多提一点,虽然我们没有指明secret_number的类型,但是因为有u32的注解,当进行比较时,Rust会推断出secret_number应该也是一个u32数字。这样一来,我们就能比较两个相同类型的变量值。

调用parse时非常容易发生错误。举例来说,如果字符串中包含A👍%,它不可能被转化为一个数字。预见到转化可能失败,parse方法也返回了一个Result类型,就像read_line方法做的那样。这里我们也想之前处理Result的方法一样,再次使用了expect方法。如果parse返回了Err这个Result枚举变量,说明输入的字符串不能转化为数字,expect方法就会终止游戏并输出我们给它的消息。如果parse成功将字符串转化为了数字,它就会返回Ok变量,expect就会返回Ok中包含的数字值。

让我们现在来运行下程序:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 0.43 secs
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 58
Please input your guess.
  76
You guessed: 76
Too big!

漂亮!尽管我们在数字前面加了空格,程序仍然能识别出我们猜的是76。试着多跑几次这个程序,来验证下不同输入时程序不同的行为:输入准确的数字,输入大的数字,输入小的数字,输入非数字。

目前我们这个游戏已经快完工了,但用户现在只能猜测依次,让我们接着为游戏添加循环功能。

Allowing Multiple Guesses with Looping 通过循环多次猜数字

loop 关键字会创建一个无穷的循环,让我们来修改代码给用户更多机会来猜数字:

// --snip--

    println!("The secret number is: {}", secret_number);

    loop {
        println!("Please input your guess.");

        // --snip--

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => println!("You win!"),
        }
    }
}

就像你看到的那样,我们把用户输入和结果验证都塞进了循环中。请注意四个空格的缩进并再次运行我们的程序。现在我们发现了一个新的问题,程序的确按照我们的要求运行了:不停地问我们要求猜数字,而我们看起来无法退出。

实际上,用户总是可以通过 ctrl-c 这个热键在任何时候终止程序。但有另一个方法可以让我们逃离这个无限循环,还记得我们在介绍parse方法时讨论过的一种情况么?当用户输入一个非数字字符串时,程序就会崩溃。用户可以通过这个方式来达到退出游戏的目的:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 1.50 secs
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 59
Please input your guess.
45
You guessed: 45
Too small!
Please input your guess.
60
You guessed: 60
Too big!
Please input your guess.
59
You guessed: 59
You win!
Please input your guess.
quit
thread 'main' panicked at 'Please type a number!: ParseIntError { kind: InvalidDigit }', src/libcore/result.rs:785
note: Run with `RUST_BACKTRACE=1` for a backtrace.
error: Process didn't exit successfully: `target/debug/guess` (exit code: 101)

输入quit可以退出程序,其它非数字输出也行。然而,这显然不是最优解。我们想要游戏能在我们猜中数字时自动终止。

Quitting After a Correct Guess 猜中后自动退出

让我们修改游戏程序,添加break语句,当用户输入正确的数字时自动退出:

// --snip--

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}

You win!后添加break能够当用户猜中正确的数字后自动退出循环,退出循环同时也意味着程序结束,因为循环是main函数最后的部分。

Handling Invalid Input 处理异常输入

让我们进一步优化游戏。当用户输入一个非数字时,比起让程序崩溃,更好的方式是让游戏忽略用户的输入,让他再猜依次。我们可以通过修改guess转化的语句来达到这个目的:

// --snip--

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

// --snip--

我们使用match表达式替换expect方法来处理错误。请牢记,parse返回的是一个Result类型,Result是个枚举,并且枚举变量是OkErr。我们在这里使用的match表达式就和之前处理cmp方法返回的Ordering一样。

如果parse能够成功将字符串转化为数字,就会返回一个Ok值,且包含了转化成的数字。Ok会去匹配第一臂的模式,然后match表达式会返回Ok中的num值。这个数字值,会传入我们新创建的guess变量中。

如果parse不能把字符串转化为数字,就会返回一个Err值,里面包含了报错消息。Err不匹配第一臂的Ok(num)模式,但它匹配第二臂的Err(_)模式。下划线_代表抓取所有值,因为在这个例子中,我们并不关注Err里的值,无论什么错误消息在里面。程序会执行第二臂中的代码continue,它会告诉程序执行下一个循环,让用户再猜依次。很有效果,我们的程序能够忽略所有parse可能遇到的报错了。

现在我们的程序已经万事俱备,能够按照我们要求的方法工作了。我们来试一下:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 61
Please input your guess.
10
You guessed: 10
Too small!
Please input your guess.
99
You guessed: 99
Too big!
Please input your guess.
foo
Please input your guess.
61
You guessed: 61
You win!

完美!最后还须要一点小修改来完成我们的猜数字游戏。我们的程序一直都会显示秘密数字,现在测试通过,我们已经不须要它了,所以我们可以把显示秘密数字的println!删掉了。最终我们的完整代码应该是下面这个样子:

use std::io;
use std::cmp::Ordering;
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;
            }
        }
    }
}

Summary 总结

在本章中,你成功创建了一个猜数字游戏,恭喜你!

这章通过简单上手的方式,向你介绍了许多新的Rust概念:letmatch,方法,关联函数,外箱的使用等等。在接下来的章节中,你将学到更多这些概念的知识。第三章包含了很多大部分程序语言有的概念,像是变量、数据类型、函数,并告诉你如何在Rust中使用它们;第四章中将探索所有权,这是一个让Rust与众不同的特点;第五章将讨论结构和方法的语法;第六章将解释枚举对象是怎样工作的。

本文标签: languageProgrammingRustChaptergame