rust编程教程26节 (rust编程语言猜数)

让我们一起通过一个实际项目来熟悉Rust吧,本章通过一个实际程序介绍Rust的基本概念,你会学到let,match关键字,函数,关联函数,使用外部crates等更多内容。接下来的章节会更详细的介绍这些概念,本章我们只是熟悉一下基础。

我们会实现一个经典的初学者编程问题:猜数字。过程是这样的:程序会先生成一个1到100之间的随机数,然后提示用户输入猜想的数字,用户输入以后程序会提示用户输入的数字是偏大还是偏小。如果猜想正确,程序会打印恭喜猜对的消息并且退出。

建立一个新项目

为了建立一个新项目,首先进入第1章里建立的projects目录,然后用cargo工具来建立新项目:

$ cargo new guessing_game

Created binary (application) `guessing_game` package

$ cd guessing_game

第一条命令cargo new,把项目名称(guessing_game)作为第一个参数,第二条命令进入新项目的目录。

我们先看下自动生成的Cargo.toml文件:

文件名:Cargo.html

[package]

name = "guessing_game"

version = "0.1.0"

authors = ["fuleitian <1774575836@qq.com>"]

edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

如果Cargo从系统环境里获取得作者信息不正确,可以修改这个文件然后保存。

就像第1章里看到的,cargo new命令会生成一个“Hello,world!”的程序给你,看下src/main.rs文件:

文件名:src/main.rs

fn main() {

println!("Hello, world!");

}

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

$ cargo run

Compiling guessing_game v0.1.0 (/Users/fuleitian/rust/guessing_game)

Finished dev [unoptimized + debuginfo] target(s) in 1.41s

Running `target/debug/guessing_game`

Hello, world!

run命令对于反复修改的项目很方便,我们在开发这个游戏程序时也会反复使用这条命令,快速测试每次的改动,没有错误才会做下一个修改。

再次打开src/main.rs文件,你会在这个文件里写全部代码。

输入猜测

猜数字游戏的第一步是请求用户输入,处理输入,然后检查输入是否是期望的。为了这点,我们允许用户输入。请把2-1的代码输入到src/main.rs里。

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

}

列表2-1: 获取用户输入并且打印的代码

这段代码有不少内容,我们一行一行来看。为了获得用户输入并且把结果输出,我们需要引入io(输入/输出)库到当前程序作用域,io库来自标准库(名字叫std):

use std::io;

默认Rust仅会引入包括在prelue里的很少的几个类型到程序作用域里,如果你想使用的类型不在prelude里,你需要用use声明来把这个类型引入进来。使用std::io库能给你一些有用的功能,包括接收用户输入的能力。

就像你在第一章里看到的,main函数是每个程序的入口:

fn main() {

fn语法声明了一个新的函数,小括号对,(),表示这个函数没有参数,花括号,{,表示函数体的开始。

在第1章里我们知道,println!是一个打印字符串到屏幕的宏:

println!("Guess the number!");

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

这段代码打印提示,表明程序需要用户输入数字;

用变量保存数值

下一步,我们会建立一个保存用户输入的地方,就像这样:

let mut guess = String::new();

现在程序开始变的有意思了!这短短一行代码有很多内容,首先注意到有一个let声明,用来建立一个变量,这里有个例子:

let foo = bar;

这行代码建立一个名叫foo的变量,并且把bar变量的值赋值过来。在Rust里变量默认是不可变的,我们会在第3章“变量和可变性”的小节里详细介绍这些概念。下面的例子演示了如何在变量名前面用mut来修饰,让变量可变:

let foo = 5; //不可变

let mut bar = 5; //可变

注意://语法开始一行注释,Rust会忽略注释里的任何内容,在第3章会详细讨论注释。

让我们回到这个游戏程序,你现在知道了let mut guess会引入一个名叫guess的可变变量,等于号(=)的另一边是guess要赋的值,就是调用String::new的返回结果,这是个返回一个新的String实例的函数。String是标准库提供的字符串类型,用来保存UTF-8格式的文字。

在::new行里,::语法表示new是String类型的一个关联函数。关联函数基于一个类型实现,本例子里就是String,而不是基于String的一个特殊实例,由一些语言把这叫做静态方法。

new函数建立一个新的空字符串,你会看到很多类型都有new函数,因为这是建立类型新值的常用名字。

总结一下,let mut guess = String::new();这行代码建立一个String类型的可变变量,并且绑定一个新空的字符串实例作为初始值,真6啊!

还记得我们程序的第一行代码用use std::io;把输入/输出功能包含进来,现在我们可以调用io模块的stdin函数:

io::stdin()

.read_line(&mut guess)

如果我们第一行没有写use std::io,我们可以把这个函数调用写成这样:std::io::stdin。stdin函数返回std::io::Stdin,代表了你的终端标准输入的句柄。

代码的下一部分,.read_line(&mut guess),调用了标准输入句柄的read_line方法来获取用户输入,还传递了一个参数给read_line:&mut guess。

read_line的功能是获取用户在标准输入设备上输入的任何内容,并把内容放到一个字符串里,因此它用一个string作为参数,这个参数必须是可变的,这样read_line方法才能把把用户输入的内容放到字符串里。

&符号表示这个参数是引用,引用可以让你在不同的地方访问同一段数据而不用把数据内容拷来拷去。引用是一个比较复杂的功能,Rust的一个主要优点就是使用引用既简单又安全。你完成这个程序不需要知道太多细节,现在你只需要知道引用就像变量一样默认不可变,所以,你必须写成&mut guess让其可变,而不是写成&guess。(第4章会更详细的介绍引用)

用Rust类型处理可能的错误

我们还在看这一行代码,虽然我们现在在讨论代码文字的第三行,这仍然是一行逻辑代码的一部分,下一部分是这个方法:

.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。枚举类型有一个固定值的集合,这些值叫做枚举成员。第6章会详细介绍枚举。

对于Result,成员是Ok或者Err,Ok成员表示操作成功,里面的Ok是成功产生的值,Err成员表示操作失败,Err包含了操作怎样或为何失败的原因。

Result类型的目的就是用来编码错误信息,Result类型的值,就像其他类型一样,会有方法定义。io::Result的实例有一个expect方法,如果io::Result的示例是个Err值,expect会引起程序崩溃并且打印你传递给expect的参数的消息。如果read_line方法返回一个Err,很可能是因为底层的操作系统发生了一个错误,如果io::Result实例是个Ok值,expect会把Ok存的值返回给你供你使用,在这个例子里,这个值就是用户在标准输入设备输入的字节数量。

如果你不调用expect,这个程序也能编译,但是会报一个告警告:

$ cargo build

Compiling guessing_game v0.1.0 (/Users/fuleitian/rust/guessing_game)

warning: unused `std::result::Result` that must be used

--> src/main.rs:10:5

|

10 | / io::stdin()

11 | | .read_line(&mut guess);

| |_______________________________^

|

= note: `#[warn(unused_must_use)]` on by default

= note: this `Result` may be an `Err` variant, which should be handled

Finished dev [unoptimized + debuginfo] target(s) in 1.20s

Rust会警告你没有使用read_line返回的Result值,提示程序没有处理可能的错误。

小区这个警告的正确的做法就是真的处理错误,因为你只是想在出错的时候让程序退出,所以调用expect就可以了,在第9章里你可以学到从错误里恢复的知识。

用println!占位符打印值

在右花括号旁边,还剩一行代码没有讨论,代码如下:

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

这行代码打印我们用来保存用户输入的字符串,花括号对,{},是一个占位符:把{}想象成一个夹着一个值的蟹钳,你可以用占位符打印多个值:第一个花括号对打印格式字符串之后的第一个值,第二个花括号对打印第二个值,以此类推。在一个调用里打印多个值可以这样做:

let x = 5;

let y = 10;

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

这行代*会码**打印 x = 5 and y = 10。

测试第一部分

让我们测试这个游戏程序的第一部分,用cargo run运行:

$ cargo run

Compiling guessing_game v0.1.0 (/Users/fuleitian/rust/guessing_game)

Finished dev [unoptimized + debuginfo] target(s) in 0.76s

Running `target/debug/guessing_game`

Guess the number!

Pease input your guess.

6

You guessed: 6

到目前为止,这个游戏的第一部分已经完成:我们能获得键盘的输入然后把它打印出来。

生成一个秘密的数字

下一步,我们需要生成一个让用户猜测的秘密数字,为了让这个游戏可以多次进行,这个数字每次都不能一样。让我们用一个1到100之间的随机数,这样游戏不会太困难。Rust标准库里现在还没有随机数的功能,然后,Rust开发团队确实提供了一个随机数包。

Cargo使用外部包的做法是优秀的,在使用rand包写代码之前,我们要先修改Cargo.toml文件把rand包作为依赖。打开文件并且把下面行加到紧跟[dependncies]段落标题下面:

文件名:Cargo.toml

[dependencies]

rand = "0.5.5"

在Cargo.toml文件里,紧跟在段落标题后面的任何内容都属于该段内容,知道另一端开始。[dependencies]段告诉Cargo项目的外部依赖包,以及该依赖包的版本。在这个文件里,我们写明了rand包及版本语法0.5.5。Cargo理解版本语法,这是一个版本数字的标准。数字0.5.5实际上是^0.5.5的缩写,意思是"任何跟版本0.5.5公共API兼容的版本"。

现在,让我们不改任何代码,直接编译项目,输出如下2-2。

$ cargo build

Updating crates.io index

Downloaded rand v0.5.6

Downloaded rand_core v0.3.1

Downloaded libc v0.2.69

Downloaded rand_core v0.4.2

Compiling libc v0.2.69

Compiling rand_core v0.4.2

Compiling rand_core v0.3.1

Compiling rand v0.5.6

Compiling guessing_game v0.1.0 (/Users/fuleitian/rust/guessing_game)

Finished dev [unoptimized + debuginfo] target(s) in 43.04s

List 2-2: 加了rand包作为依赖后的cargo build命令输出

你可能会看到不同的版本数字(但是它们是兼容的,感谢版本语法!),行的顺序也可能不一样。

现在我们有了外部依赖,Cargo会从服务器同步所有包的最新版本号,实际上就是从Crates.iof复制一份目录。Crate.io是Rust生态里人们上传开源Rust项目,供其他人使用的地方。

Cargo在更新目录以后,Cargo检查[dependencies]段并且*载下**还没有的包。本例中,尽管我们只把rand列为依赖,Cargo还抓取了libc以及rand_core包,因为rand依赖这两个包来工作。*载下**完这些包以后,Rust先编译这些依赖包,然后再使用已经可用的依赖来编译项目。

如果你没做任何改动,立即再次运行cargo build,你不会看到除了Finished行外的其他内容。Cargo知道依赖已经*载下**并且编译过了,并且你没有改变Cargo.toml内容。Cargo还知道你没有改动你的代码,所以不用重新编译任何东西,因此什么都不要做,直接退出了。

如果你打开src/main.rs文件,试着改点东西,然后保存并重新编译,你会看到两行输出:

$ cargo build

Compiling guessing_game v0.1.0 (/Users/fuleitian/rust/guessing_game)

Finished dev [unoptimized + debuginfo] target(s) in 0.27s

这两行显示了Cargo仅仅编译了有改动的src/main.rs文件,依赖并没有改变,因此Cargo知道它可以重用已经*载下**并且编译过的依赖包,只编译你改过的代码。

使用Cargo.lock文件保证编译的可重复性

Cargo有一个机制,可以保证每次你或其他人每次重新编译是同样的内容:Cargo会使用跟你写明版本兼容的依赖,除非你另外指出。比如说,如果下周rand包0.5.6版本出来了,包含了一个重要的bug修复,但是改动同时会让你的代码编译不过,这个要怎么办?

这个问题的答案就是使用Cargo.lock文件,这个文件是第一次你运行Cargo build命令时生成的,就在你的guess_game目录里。当你第一次编译项目时,Cargo会找出全部符合的依赖的版本并且把内容写到Cargo.lock文件里,未来你再编译项目时,Cargo会发现Cargo.lock文件已经存在,就会使用文件里写明的依赖版本,而不是再去找一遍版本。这点让你能自动重复编译,换句话说,你的项目就停留在版本0.5.5,除非你明确升级,多谢Cargo.lock文件。

更新新版本的包

当你想要更新包时,Cargo提供了另一个命令,update,这条命令会忽略Cargo.lock文件,并且找出符合Cargo.toml写明的依赖包的最新版本。如果找到了,Cargo会把这些版本写到Cargo.lock文件里。

但是默认Cargo只会找那些大于0.5.5且小于0.6.0的版本,如果rand包发布了两个新版本,0.5.6和0.6.0,运行cargo update你会看到下面的输出:

$ cargo update

Updating crates.io index

Updating rand v0.5.5 -> v0.5.6

这时你看到发现记录rand包版本的Cargo.lock文件现在已经是版本0.5.6了。

如果你想用0.6.0或者0.6.x系列版本的rand包,你必须更新Cargo.toml文件成这样:

[dependencies]

rand = "0.6.0"

下次运行cargo build,Cargo会更新可用包目录,重新检视你新版本rand包的需求。

关于Cargo以及其生态系统还有很多要讲的东西,我们会在第14章讨论,但现在,这些就是你需要知道的全部内容。Cargo让库的重用变得很容易,所有Rust程序员们可以利用其他包来写更小的程序。

生成随机数

现在你已经把rand包加到Cargo.toml里了,让我们开始使用rand包。下一步是更新src/main.rs文件,如清单2-3所列。

文件名: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!("Pease input your guess.");

let mut guess = String::new();

io::stdin()

.read_line(&mut guess)

.expect("Failed to read line");

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

}

清单2-3:添加生成随机数的代码

我们一开始增加了use行:use rand::Rng。Rng特性定义了随机数生成的实现方法,这个特征必须引入到作用域里,第10章会详细介绍特征。

接下来,我们在中间增加了两行代码,rand::thread_rng函数会提供给我们一个将要使用的随机数生成器:一个局限于当前进程并且由操作系统提供随机数种子的随机数生成器。然后我们调用随机数生成器gen_range方法,这个方法由Rng特征定义,用use rand::Rng声明引入到作用域。gen_range方法接受两个数字作为参数,并且产生一个在两个数字区间的随机数,区间是左闭右开的,意思是包括下边界,但是不包括上边界的数字,因此我们需要指定1到101来请求产生1到100的随机数。

注意:你还不需要知道要使用包里的那个特性调用那个方法和函数,使用包的说明在每个包的文档里,另一个技巧是使用Cargo doc --open命令来生成依赖提供的文档,并且用浏览器打开。比如说你对rand包其他功能感兴趣,可以运行cargo doc --open并且点击左边侧栏的rand条目来查看。

我们增加的第二行代码是打印产生的秘密数字,当我们在开发程序的过程里这很有用,但最终版本我们会删掉这行代码,程序刚开始运行就把答案显示出来可不是一个游戏该做的事情。

试着运行几次这个程序:

$ cargo run

Finished dev [unoptimized + debuginfo] target(s) in 0.01s

Running `target/debug/guessing_game`

Guess the number!

The secret number is: 71

Pease input your guess.

11

You guessed: 11

$ cargo run

Finished dev [unoptimized + debuginfo] target(s) in 0.01s

Running `target/debug/guessing_game`

Guess the number!

The secret number is: 35

Pease input your guess.

3

You guessed: 3

你应该会获得不同的随机数,所有随机数都应该在1到100之间,干得好!

比较秘密数字和猜测的数字

文件名:src/main.rs

use std::io;

use rand::Rng;

use std::cmp::Ordering;

fn main() {

// --删除--

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

match guess.cmp(&secret_number) {

Ordering::Less => println!("Too small!"),

Ordering::Greater => println!("Too big!"),

Ordering::Equal => println!("You win!"),

}

}

清单2-4:处理两个数字比较可能的返回值

第一个新的改动是另一条use声明,把std::cmp::Order从标准库里引入到作用域。就像Result,Ordering是另一个枚举类型,但是枚举值是Less,Greater,以及Equal,这是三种比较两个值会出现的结果。

然后我们在底下增加了使用Ordering类型的5行代码,cmp方法比较两个值,能够在任何能做比较的地方使用。它接收一个要比较的引用:这里是把guess跟secret_number作比较,然后返回一个我们用use声明引入的Ordering枚举的值,根据调用cmp比较guess和secret_number值返回的结果,我们用match表达式决定下一步要做什么。

match表达式由分支(arms)组成,一个分支由条件以及match表达式值符合这个条件时需要执行的代码组成。Rust获取给match的值,依次检查arm的条件。match是很有用的功能,能让你表达多种代码可能遇到的情况,并且保证全部情况都得到了处理。相应的这些功能会在第6章和第18章里详细介绍。

让我们看下这个用了match表达式的例子是怎样运行的,比如说用户猜了数字50,随机生成的秘密数字是38。当代码比较50和38时,因为50大于38,所以cmd方法会返回Ordering::Greater。match表达式获得这个Ordering::Greater值,开始检查每个分支的条件。第一个分支条件是Ordering::Less,Order::Greater值跟这个条件不匹配,因此它会忽略这个分支的代码,检查下一个分支。下个分支Ordering::Greater确实是匹配的,这个分支的代*会码**被执行,在屏幕上打印出Too big!消息。然后match表达式结束了,不需要检查最后一个分支。

然而,清单2-4的代码不能编译,让我们试下:

$ cargo build

Compiling guessing_game v0.1.0 (/Users/fuleitian/rust/guessing_game)

error[E0308]: mismatched types

--> src/main.rs:21:21

|

21 | match guess.cmp(&secret_number) {

| ^^^^^^^^^^^^^^ expected struct `std::string::String`, found integer

|

= note: expected reference `&std::string::String`

found reference `&{integer}`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0308`.

error: could not compile `guessing_game`.

To learn more, run the command again with --verbose.

错误信息的核心意思是有不匹配的类型,Rust是静态的强类型语言,但是它也有类型推断的能力,当我们写代码let mut guess = String::new(),Rust能够推断出guess应该是String类型,因此不需要我们明确写出类型。对于secret_number则是数字类型。能够保存1到100数字的几个类型包括:i32,32位数字;u32,无符号32位数字;i64,64为数字;以及其他类型。Rust默认数字类型是i32,因此secret_number就是i32类型,除非你添加了类型信息,从而让Rust推断成不同的数字类型。出错的原因就是Rust不能把字符串和数字类型作比较。

最终,我们希望把程序读进来的字符串类型转换成一个数字类型,然后我们再跟秘密数字作比较,我们在main函数里增加下面两行来完成这点:

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允许我们用新的值来屏蔽前面guess的值,这个特性经常用在你想把一个变量从一个类型转成另一个类型的时候,屏蔽让我们能够重用guess这个变量名,而不是强迫我们起两个不同的名字,比如说guess_str和guess。(第3章会仔细讲述变量屏蔽)

我们把guess绑定到表达式guess.trim().parse()返回的值,表达式里的guess是旧的用来存储输入字符串的guess。String实例的trim方法会去掉字符串行头和行尾的任何空白字符。尽管u32只能包含数字字符,用户必须要按回车键来满足read_line调用。例如,用户输入了5然后敲了会车键,guess看起来是这样的:5\n。\n代表“新行”,是敲回车键输入的结果。trim方法会去掉\n,结果就只是5。

String的parse方法会把一个字符串转换成一些类型的数字,因此这个方法可以转化几种数字类型,我们需要用let guess:: u32来告诉Rust我们期望的数字类型。guess后面的冒号(:)告诉Rust我们标注了变量的类型。Rust有一些内置的数字类型,这里的u32室无符号32位整型数。对于小的正数这是一个很好的默认类型选择。在第3章里你会看到其他数字类型,另外,这个程序里的u32标注以及和secret_number的比较意味着Rust会把secret_number也推断成u32,因此现在这两个值的比较就是同一种类型了!

parse调用会很容易发生错误,如果,比如说,字符串包含了A%,就不可能把字符串转成数字,因此可能会失败,parse方法返回Result类型,就像read_line那样的做法(在前面“用Result类型处理可能错误”里刚讨论过)。我们同样再用expect方法来处理Result,如果因为parse不能从字符串建立一个数字,就返回一个Err的Result变量,expect调用会让游戏崩溃,并且打印我们给它的消息。如果parse能够成功把字符串转成数字,它会返回Result类型的Ok变量,expect就会从Ok里返回我们想要的数字的值。

现在让我们运行程序!

$ cargo run

Finished dev [unoptimized + debuginfo] target(s) in 0.01s

Running `target/debug/guessing_game`

Guess the number!

The secret number is: 93

Pease input your guess.

76

You guessed: 76

Too small!

很好,即使我们在猜的数字前面加空格,程序仍然能够打印出用户猜的是76。运行几次这个程序,输入不同的内容来来验证不同的行为:输入正确的数字,输入的数字很大,以及输入的数字很小。

我们现在有一个几乎能玩的游戏了,但是用户只能猜一次,让我们加个循环来改正这点。

使用循环来允许多次猜测

loop关键字会建立一个无限循环,我们会增加一个loop来给用户更多猜数字的机会:

文件名:src/main.rs

// --忽略--

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

loop{

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

// --忽略--

match guess.cmp(&secret_number) {

Ordering::Less => println!("Too small!"),

Ordering::Greater => println!("Too big!"),

Ordering::Equal => println!("You win!"),

}

}

就像你看到的,我们已经把输入提示及以后的代码放到一个循环里,记得把loop里的代码行缩进4个空格,重新运行程序,会注意到程序正严格按照我们让它做的运行,出现了一个新问题:程序会一直让用户再次输入!看起来用户不能退出!

用户可以用快捷键ctrl-c来中断程序,还有一种办法来中断这个喂不饱的怪物,就像在“比较秘密数字和猜测的数字”里讨论parse方法提到的:如果用户输入了一个非数字答案,程序会崩溃。用户可以利用这一点来退出,就像这样:

$ cargo run

Finished dev [unoptimized + debuginfo] target(s) in 0.01s

Running `target/debug/guessing_game`

Guess the number!

The secret number is: 19

Pease input your guess.

45

You guessed: 45

Too big!

Pease input your guess.

60

You guessed: 60

Too big!

Pease input your guess.

19

You guessed: 19

You win!

Pease input your guess.

quit

thread 'main' panicked at 'Please type a number!: ParseIntError { kind: InvalidDigit }', src/main.rs:20:26

note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

输入quit确实退出了游戏,但是其他非数字输入也可以,这不是个好办法,我们是希望当输入正确答案时游戏自动停止。

输入正确的数字退出

当用户赢了游戏时要退出游戏,我们用break语句来完成。

文件名:src/main.rs

$ cargo run

Finished dev [unoptimized + debuginfo] target(s) in 0.01s

Running `target/debug/guessing_game`

Guess the number!

The secret number is: 52

Pease input your guess.

52

You guessed: 52

You win!

在You win!后面加上break代码,这样当用户输入正确的数字时就会让程序退出循环,退出循环也就意味着退出程序,因为循环是main的最后一部分。

处理无效输入

为了进一步重构游戏的行为,我们让游戏忽略非数字输入,这样用户就可以继续猜了,而不是让程序崩溃。我们可以通过修改把guess从string转成u32那行代码来实现,如清单2-5所示:

//--忽略--

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

//--忽略--

清单2-5:忽略非数字输入,请求再次输入而不是让程序崩溃

从调用expect切换到match表达式,可以从出错时让程序崩溃变成处理错误。记住parse返回Result类型,Result类型是个枚举变量,有Ok或者Err的枚举值,我们在这里用match表达式,就像我们处理cmp方法的Ordering结果一样。

如果parse能成功把一个字符串转成一个数字,就会返回一个Ok值,里面携带了转换后的数字。Ok值会跟第一个分支匹配,match表达式就会直接返回parse方法放到Ok值里的num值,在右边的这个值就会放在我们正在建立的新的guess变量里面。

如果parse不能把字符串转成数字,就会返回一个Err值,里面包含了更多的关于错误的信息,Err值不匹配第一个分支的Ok(num),但是匹配第二个分支的Err(_)模式,下划线,_,代表所有值;在这个例子里,我们想要匹配全部Err值,不管里面是什么信息。于是程序会执行第二个分支代码,continue,告诉程序开始下一次循环,请求再次输入。所以程序就忽略了parse可能发生的全部错误了。

现在程序应该像我们期望那样运行了,让我们试下:

$ cargo run

Finished dev [unoptimized + debuginfo] target(s) in 0.03s

Running `target/debug/guessing_game`

Guess the number!

The secret number is: 87

Pease input your guess.

10

You guessed: 10

Too small!

Pease input your guess.

99

You guessed: 99

Too big!

Pease input your guess.

foo

Pease input your guess.

87

You guessed: 87

You win!

不错,我们再改一个地方,就能完成这个猜数字的游戏了。还记得程序还在打印要猜的数字么,这点用来测试很好,但是会毁掉整个游戏。让我们把打印秘密数字的println!删掉,清单2-6是最终代码。

文件名:src/main.rs

use std::io;

use rand::Rng;

use std::cmp::Ordering;

fn main() {

println!("Guess the number!");

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

loop{

println!("Pease 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;

}

}

}

}

清单2-6:完整游戏代码

总结

到此为止,你已经成功完成一个猜数字游戏,恭喜!

这个项目是一个动手操作的项目,用来讲解Rust的一些新概念:let,match,方法,相关函数,使用外部包,以及更多内容。在接下来的几章里,你会更详细的学习这些概念。第3章介绍了大多数语言都有的概念,比如说变量,数据类型,以及函数,并且演示了如何在Rust里使用。第4章介绍了所有权,一个让Rust如众不同的特点。第5章讨论了结构体和方法语法,第6章介绍了枚举是如何工作的。