跳转至

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 的成员是 OkErrOk 表示操作成功,内部包含成功时产生的值。Err 成员表示操作失败,包含失败的前因后果。

io::Result 实例有 expect 方法,如果返回值是 Errexpect 会导致程序崩溃,同时输出它的参数。

这里如果不调用 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 也是一个枚举,它的成员是 LessGreaterEqualcmp 方法用来比较任何可比较的值,参数是一个被比较值的引用,返回一个 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,
};

ResultOrdering 都是枚举,所以可以使用 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 的概念:letmatch、方法、关联函数、使用外部 crate 等。

接下来的几章会深入学习这些概念。


最后更新: May 10, 2021