0x02 Guessing Game Tutorial*
通过一个简单的猜数游戏快速上手 Rust
新建项目 guessing_game。
处理一次猜测*
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 库。
use std::io;
使用变量储存用户输入。
let mut guess = String::new();
使用 let 创建变量。Rust 中,变量默认不可变,使用 mut 使变量可变。
String::new 调用 String 类型的 关联函数(associated function),即静态方法。
new 创建了一个空字符串,将变量绑定到这个值。
下面使用 io 库中的函数 stdin 获取用户输入。
io::stdin().read_line(&mut guess)
.expect("Failed to read line");
调用 read_line 从标准输入句柄获取用户输入,存入可变字符串变量。
& 表示一个引用,Rust 的一个主要优势就是安全而简单地操作引用。与变量一样,默认不可变,需要写成 &mut guess 使其可变。
函数会返回一个 Result 类型的值,在这里是 io::Result。
Result 类型是枚举,枚举类型有固定集合的值,即枚举的成员。Result 的成员是 Ok 和 Err,Ok 表示操作成功,内部包含成功时产生的值。Err 成员表示操作失败,包含失败的前因后果。
io::Result 实例有 expect 方法,如果返回值是 Err,expect 会导致程序崩溃,同时输出它的参数。
这里如果不调用 expect,编译时会有警告,未处理可能的错误。消除警告的正确做法是编写错误处理代码,这里用 expect 是在出现问题时立即崩溃,之后会学习如何从错误中恢复。
println!("You guessed: {}", guess);
println! 第一个参数是格式化字符串,{} 是预留在特定位置的占位符。
生成一个秘密数字*
Rust 标准库未包含随机数功能,提供了一个 rand crate。
crate 是一个 Rust 代码包,现在正在构建的项目是一个 二进制 crate,生成一个可执行文件。rand crate 是一个 库 crate,可以包含任意能被其他程序使用的代码。
修改 Cargo.toml 文件,引入 rand 依赖。
[dependencies]
rand = "^0.5.5"
Cargo 可以使用语义化版本,^0.5.5 表示任何与 0.5.5 版本 API 兼容的版本,可以简写为 0.5.5。不修改代码,构建项目,Cargo 就会更新 registry,默认是 crates.io,更新完后 Cargo 会检查依赖并下载需要的 crate。下载完成后,Rust 会编译依赖,然后使用依赖编译项目。
Cargo.lock 用来确保任何人任何时候重新构建代码都会产生相同的结果:Cargo 只会使用指定的依赖版本,除非显式地修改。
当需要升级 crate 是,Cargo 提供了 update 命令,会忽略 Cargo.lock 文件,并计算出符合 Cargo.toml 声明的最新版本。如果成功,Cargo 会把这些版本写入 Cargo.lock 文件。
不过 Cargo 默认只会寻找 (0.5.5, 0.6.0) 之间的版本,如果想使用 0.6.0 的就需要更新 Cargo.toml。
下面就可以使用 rand 生成随机数。
use rand::Rng;
...
let secret_number = rand::thread_rng().gen_range(1, 101);
...
Rng 是一个 trait,定义了随机数生成器应实现的方法,第十章会详细介绍 trait。
rand::thread_rng 函数提供实际使用的随机数生成器:位于当前执行线程的本地环境中,并从操作系统获取 seed。调用随机数生成器的 gen_range 方法,它由引入到作用域的 Rng trait 定义。
使用 cargo doc --open构建所有本地依赖提供的文档,并在浏览器中打开。
比较猜测数和秘密数*
use std::cmp::Ordering;
...
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
...
首先引入 std::cmp::Ordering 类型,Ordering 也是一个枚举,它的成员是 Less、Greater 和 Equal。cmp 方法用来比较任何可比较的值,参数是一个被比较值的引用,返回一个 Ordering 成员。
使用 match 表达式,决定接下来做什么。match 表达式由分支构成,一个分支包含一个模式(pattern)和表达式与分支模式相匹配时应该执行的代码。
然而这里还不能编译,会报错类型不匹配。Rust 有一个静态强类型系统,同时也有类型推断。之前把 guess 绑定到 String::new(),Rust 推断 guess 应该是 String。而 secret_number 是数字类型,默认使用 i32。报错原因在于 Rust 不会比较字符串和数字类型。
所以需要把输入的 String 转换成数字类型。
let guess: u32 = guess.trim().parse()
.expect("Please type a number!");
这里又创建了一个 guess 变量,Rust 允许用一个新值来隐藏(Shadow)之前的值。常用于类型转换的场景,允许复用变量名。
这里将 guess 绑定到表达式值上,表达式中的 guess 是输入的 String 类型。使用 parse 将字符串解析成数字,通过这里的 : u32 指定解析的具体数字类型。这里的 u32 以及后面与 secret_number 的比较,Rust 会推断出 secret_number 也是 u32 类型。
与之前的 read_line 类似,parse 函数也会访问 Result 类型。
使用循环多次猜测*
使用 loop 关键字创建一个无限循环。
loop {
// input
// match
Ordering::Equal => {
println!("You win!");
break;
}
}
使用 break 退出循环。
处理无效输入*
为了不在输入非数字时直接崩溃退出程序,可以选择忽略非数字,让用户继续输入。
let guess: u32 = match guess.trim().parse() {
Ok(num) = num,
Err(_) = continue,
};
Result 和 Ordering 都是枚举,所以可以使用 match 语句指定处理方法。parse 处理成功后返回 Ok 包含的数字 num,不成功则会匹配 Err(_),_ 是通配符,会匹配所有的 Err 值,continue 表示进入下一次循环,请求下一次猜测。
最后就得到了完整代码
// guessing_game/src/main.rs
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1, 101);
// println!("Generated secret number {}", secret_number);
let mut cnt: u32 = 0;
loop {
cnt += 1;
println!("Input");
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(_) => {
println!("Please type a number!");
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! You cost {} times", cnt);
break;
}
}
}
}
总结*
通过简单的游戏,介绍了 Rust 的概念:let、match、方法、关联函数、使用外部 crate 等。
接下来的几章会深入学习这些概念。