目录
1. 创建项目
2. 猜数的输入
3. 随机数生成
3.1 rand库依赖
3.2 随机数生成
4. 猜数和随机数的比对
4.1 std::cmp::Ordering类型
4.2 match表达式(expression)
4.3 输入类型的转换
5. 支持多次猜测(使用循环)
6. 错误输入的处理
本章节以一个精心设计的实际工程项目,先来初次尝试一下rust语言编程和工程应用的实践。该项目是一个:猜数游戏,主要工作流程如下:
1)程序生成1~100之间的一个随机整数;2)用户输入猜测的数字;3)程序输出猜测的数字大于或小于生成数;4)用户猜测正确,结束运行,用户猜测错误,可重复2、3步骤。
1. 创建项目
# cargo new guessing_game && cd guessing_game
Created binary (application) `guessing_game` package
# cargo run
Compiling guessing_game v0.1.0 (/doc/jiangxiaoqing/rust/chapter2/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/guessing_game`
Hello, world!
# ls
Cargo.lock Cargo.toml src target
# tree
.
├── Cargo.lock
├── Cargo.toml
├── src
│ └── main.rs
└── target
├── CACHEDIR.TAG
└── debug
├── build
├── deps
│ ├── guessing_game-2e7ea747ef88670f
│ └── guessing_game-2e7ea747ef88670f.d
├── examples
├── guessing_game
├── guessing_game.d
└── incremental
└── guessing_game-1vpxsh8m6pzam
├── s-gfeqim8j6x-ein778-3lplmqmartfnp
│ ├── 1ez1ge870eb6gq7l.o
│ ├── 1sx3ehn655us4o1e.o
│ ├── 1u60bgs93g3f9qgi.o
│ ├── 2zd4w7ck950yptxq.o
│ ├── 4ugvrlcdoppn14n.o
│ ├── 57xo5z3lc17i2l94.o
│ ├── dep-graph.bin
│ ├── query-cache.bin
│ └── work-products.bin
└── s-gfeqim8j6x-ein778.lock
9 directories, 18 files
生成一个目标项目,可以直接先运行一下,后面步骤编辑main.rs来完成该项目。
2. 猜数的输入
首先,我们来处理用户输入的交互部分:允许用户输入一个数字
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);
}
以上代码使用了rust标准库(std)中的io库,默认情况下,rust在标准库中的某些项能被自动载入。而非自动载入的需要用use语句来引入。如代码,io库中的某些功能能让我们处理用户输入。
let可以定义新的变量,并绑定到后面的字符串对象。这里的mut标识该变量是允许修改的,后面第3章节会解释。String::new函数会创建并实例化一个空的字符串。
std::io::stdin()函数,返回一个std::io::Stdin的对象实例,代表一个到terminal终端标准输入的句柄。后面的.read_line()函数接收用户的一行输入,并append到字符串对象guess中。& 符号指示参数使用引用,而不需要拷贝参数。引用 是rust语言中的一个比较复杂的特性,默认情况下是不可变 的,因此这里使用了&mut 来使其是可修改的。
readline()函数返回一个io::Result() 的类型对象。rust标准库中包含一系列的Result命名的类型。io中的Result是一个枚举类型,包含固定数量变量的一个集合。枚举类型通常与match 一起使用,match是一个条件匹配。这里的Result可以取值Ok 和Err 。
Ok 表示当前操作是成功的,Ok内部存储了生成的值;Err 表示当前操作失败,Err内部存储了失败的信息。
io::Result对象也有方法,例如expect方法。如果io::Result是一个Err对象,则程序crash,并打印一条expect调用参数的字符串。如果不调用expect()方法,则编译会报一个warning,警告未处理可能发生的错误。
3. 随机数生成
3.1 rand库依赖
使用一个外部库rand crate,来生成伪随机数。可以多次玩猜数游戏。
cargo管理了线上(crates.io)的库注册,以及本地工程的crates依赖,首先我们要添加一个rand依赖到cargo的配置文件Cargo.toml中。
在配置文件的dependencies section中添加依赖的crate名和版本号。这里的0.8.5 是一个0.8.*最低版本以及不高于0.9.0版本的语义,而非特定版本,即:高于0.8.5版本小于0.9版本的都可以。cargo会从线上拉取一个最新的满足版本要求的rand下来到${home}/.cargo目录中。在这个过程中,rand自身依赖的crates同样被递归拉下来了。 这种工程的库和版本依赖管理,对于rust工程开发非常重要。
# cargo build
Updating crates.io index
Downloaded ppv-lite86 v0.2.17
Downloaded getrandom v0.2.8
Downloaded rand_chacha v0.3.1
Downloaded rand v0.8.5
Downloaded rand_core v0.6.4
Downloaded cfg-if v1.0.0
Downloaded libc v0.2.137
Downloaded 7 crates (791.9 KB) in 0.97s
Compiling libc v0.2.137
Compiling cfg-if v1.0.0
Compiling ppv-lite86 v0.2.17
Compiling getrandom v0.2.8
Compiling rand_core v0.6.4
Compiling rand_chacha v0.3.1
Compiling rand v0.8.5
Compiling guessing_game v0.1.0 (/doc/jiangxiaoqing/rust/chapter2/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 4.55s
# ls ~/.cargo/registry/src/github.com-1ecc6299db9ec823/
cfg-if-1.0.0 getrandom-0.2.8 libc-0.2.137 ppv-lite86-0.2.17 rand-0.8.5 rand_chacha-0.3.1 rand_core-0.6.4
- Cargo.lock文件包含了什么?
留意,在项目根目录有一个Cargo.lock文件,该文件的作用是什么呢?
Cargo.lock本质上一个crates依赖的本地缓存,会在第一次cargo build时产生。 假设首次build时,从crate.io拉到了0.8.5版本的rand,而过了一段时间之后,0.8.6版本在线上被发布,但该版本无法在我们的guessing_game中运行。cargo本身总是拉取符合条件的最高版本,但存在Cargo.lock时,则优先根据历史缓存中的版本来拉取和构建。这保证了我们工程总是可以重复构建成功的,即使线上registry中发布了符合我们版本期望但却导致我们工程无法编译的crates版本。那么,如何更新到线上最新版本呢?通过执行cargo update拉拉取线上符合条件的最新版本的各个crates。
我们将在14章节,重点介绍Cargo生态。
3.2 随机数生成
use std::io;
use rand::Rng;
fn main() {
println!("Guest the number:");
let secret_number = rand::thread_rng().gen_range(1..101);
println!("The secret number is: {}", secret_number);
println!("Please input your guesss.");
let mut guess = String::new();
io::stdin().read_line(&mut guess).expect("Failed to read line");
println!("You guessed: {}", guess);
}
rand::Rng中的Rng 是一个trait ,在第10章将介绍什么是trait 。Rng中定义了生成随机数的方法。
thread_rng函数返回一个随机数发生器对象,只跟当前线程相关的随机数发生器,使用了操作系统决定的随机种子。gen_range()函数利用随机数发生器,生成指定参数范围内的伪随机数。如上:1..101表示1到100之间的数字。
range表达式start..end的范围时[start, end),即不包括end。也可以写作,start..=end,表示[start, end]。
小技巧:
使用cargo doc --open会编译出本地工程以及依赖的所有crates的相关使用文档,就可以查询特定crate中函数的使用手册。也可以在crate.io线上搜索相关库的crate文档手册。
4. 猜数和随机数的比对
有了生成的随机数和用户输入的数字,就可以进行比对。
use std::io;
use rand::Rng;
use std::cmp::Ordering;
fn main() {
println!("Guest the number:");
let secret_number = rand::thread_rng().gen_range(1..101);
println!("The secret number is: {}", secret_number);
println!("Please input your guesss.");
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!");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
println!("You guessed: {}", guess);
}
4.1 std::cmp::Ordering类型
该类型是标准库提供的一个枚举类型,可以取值Less、Greater、Equal 。cmp 函数可以比较任意两个“可比较”的类型值,并返回一个std::cmp::Ordering类型对象。
4.2 match表达式(expression)
类似C语言中的switch...case语句,match表达式包含多个arms(case语句?)。每个arm是一个匹配模式,rust会依次尝试匹配每一个arm。第6和第18章节将详细介绍。
4.3 输入类型的转换
由于输入的捕获是一种字符串类型,无法与生成伪随机数的数字类型进行匹配比对。rust是强类型的编程语言。生成的随机数默认是一个i32类型,(32位的有符号整型数字)。
第2个定义的guest将字符串的guest给覆盖了,trim()函数是字符串guest对象的一个方法,将我们输入的字符串前后的空格给过滤掉,然后用parse()尝试转换成u32类型的数字。
5. 支持多次猜测(使用循环)
到此,我们可以进行一次猜测,接下来使用rust的循环语句loop运行用户进行多次猜测,直到猜成功。
use std::io;
use rand::Rng;
use std::cmp::Ordering;
fn main() {
println!("Guest the number:");
let secret_number = rand::thread_rng().gen_range(1..101);
// println!("The secret number is: {}", secret_number);
loop {
println!("Please input your guesss.");
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!");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
//println!("You guessed: {}", guess);
}
}
上述代码,用户无限猜测,直到猜数成功,或者输入非法(非数字)。parse()失败,程序会直接crash退出,并输出错误提示“Please type a number!”。
6. 错误输入的处理
到目前为止,用户输入非法(非数字)时会导致程序直接crash(输出一个错误消息)。为了不使程序崩溃,更友好提示用户重新输入,做如下改造:忽略用户的非法输入,并运行再次输入。
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
parse()返回的是一个Result类型对象,而Result对象是一个具有Ok和Err两个枚举值的枚举类型。这样,我们同样可以用match表达式(match是表达式expression,而非语句statement) ,来匹配并区分处理。
如果parse()成功将字符串转换为一个数字,将返回一个包含结果数字的Ok枚举值,则直接返回该数字;如果失败,返回一个包含错误消息的Err枚举值,Err(_)中下划线的语义是匹配任意值,即匹配所有类型的Err,不论内部信息如何。continue将返回到loop循环的下一轮循环开始处运行。 因此,上述代码,忽略了parse()遇到的所有错误。
最终版本代码如下:
use std::io;
use rand::Rng;
use std::cmp::Ordering;
fn main() {
println!("Guest the number:");
let secret_number = rand::thread_rng().gen_range(1..101);
// println!("The secret number is: {}", secret_number);
loop {
println!("Please input your guesss.");
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!");
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
//println!("You guessed: {}", guess);
}
}
运行测试:
# cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/guessing_game`
Guest the number:
Please input your guesss.
60
Too small!
Please input your guesss.
sd
Please input your guesss.
90
Too big!
Please input your guesss.
75
Too small!
Please input your guesss.
83
Too big!
Please input your guesss.
78
You win!