02 - 使用 Rust 开发第一个游戏

一、说明

这个游戏是一个简单的猜数字游戏,记得很早的时候在学小甲鱼老师的 Python 课程时,甲鱼老师也是从一个猜数字游戏讲起。这个猜数字游戏能够让我们快速熟悉 Rust!可能会接触到诸如 let、match、类型方法、关联函数以及外部依赖库的知识。
我们将完成一个经典的初学者编程挑战:猜数字游戏,它会首先生成一个 1 到 100 之间的整数,并紧接着请求玩家对这个数字进行猜测。假如玩家输入的数字与随机数不同,那么程序将给出数字偏大或偏小的提示。而假如玩家猜中了我们准备的数字,那么程序就会打印一段祝贺信息并随之退出。

二、创建一个新项目

使用 Cargo 创建一个新项目

cargo new --vcs none 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);
}

四、代码解析

第一行的 use std::io 是为了获得用户的输入并将其打印出来而引入的一个库模块,std::io 库包含了许多有用的功能,使用它来获得用户输入的数据。

println! 是一个宏,而不是一个函数,用于将字符串打印在屏幕上。创建变量以 let 开头,在 Rust 中,变量都是默认不可变的,现在只需要知道必须使用 mut 关键字来声明一个变量是可变的

let foo = 5;        // foo 是不可变的
let mut bar = 5;    // bar 是可变的

上面代码中的 // 表示注释,let mut guess = String::new(); 语句会创建一个名为 guess 的可变变量,调用函数 String::new 后返回的结果:一个新的 String 实例,String 是标准库中的一个字符串类型,它在内部中使用 UTF-8 格式的编码并可以按照需求扩展自己的大小。:: 表明 new 是 String 的一个关联函数,关联函数在某些语言中称之为静态方法。
read_line() 函数会返回一个 io::Result 值,在 Rust 标准库中,可以找到许多以 Result 命名的类型,它们通常是各个子模块中的 Result 泛型的特定版本,比如这里的 io::Result。Result 是一个枚举类型,枚举类型由一系列固定的值组合而成。Result 有 Ok 和 Err 两个枚举值,Ok 表示操作成功,并附带代码产生的结果值,而Err 表示操作失败,并附带引发失败的原因。Result 类型的值定义了一系列方法,例如此例中的 expect,假如 io::Result 实例的值是 Err 结果,那么 expect 方法就会中断当前的程序,并将传入的字符串参数显示出来。read_line 方法有可能因为底层操作系统的错误而返回一个 Err 结果。相应地,假如 io::Result 实例的值是 Ok,那么 expect 就会提取出 Ok 附带的值并将它作为结果返回给用户。如果没去调用 expect 方法,编译能通过,但是编译器会给出一个警告:

image

编译器告诉我们 read_line 方法返回的 Result 值未被处理,这意味着我们的程序没有对潜在的错误进行处理。消除警告简单做法是调用 expect 方法,当程序发生错误时可立即中止程序运行并退出,以后会来探讨 Rust 的错误处理机制。

特别注意,println! 是一个宏,而不是一个函数,它通过占位符 {} 来输出对应的值,尝试运行下代码:

image

五、生成一个保密数字

要随机生成一个数字作为保密数字,需要用到 rand 包,Rust 开发团队并没有把类似随机生成功能内置到标准库中,而是选择它作为 rand 包提供给用户。

cargo 能够为我们更好地来管理包,在 Cargo.toml 文件中将 rand 包声明为依赖

[package]
name = "guessing_game"
version = "0.1.0"
edition = "2021"

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

[dependencies]
rand = "0.3.14"

rand 的值为 0.3.14,指定它的版本号,其实它的值为 ^0.3.14,表示 任何与 0.3.14 版本公共 API 相兼容的版本。接下来使用 cargo build 进行构建

image

image

如果发生 SSL 连接错误,可更换 Cargo 源为国内镜像源或者自己开节点 (若还报 SSL 连接错误请更换节点)。上图中显示的编译顺序可能会发生变化,显示的版本号也可能与我们指定不同,但有一点,Cargo 会按照标准的语义化版本系统 (Semantic Versioning,SemVer) 来理解所有的版本号,按照 SemVer 约定,会一直与我们的代码保证兼容。

现在程序有一个外部依赖,Cargo 会从 registry 获取所有可用库的最新版本信息,而这些信息是从 crates.io 上复制过来的。在 Rust 生态中,crates.io 是人们用于分享各种各样开源 Rust 项目的网站。

Cargo 会在更新完 registry 后开始逐条检查 [dependencies] 区域中的依赖,并下载当前缺失的依赖包,在下载完所有依赖所需的包后,Rust 就会开始编译它们,并基于这些依赖编译我们自己的项目。

通过 Cargo.lock 文件确保构建是可重现的

当第一次使用 cargo build 构建进,Cargo 会依次遍历我们声明的依赖及其对应的语义化版本,找到符合要求的具体版本号,并将它们写入到 Cargo.lock 文件中,随后再次构建时,会优先检索 Cargo.lock 文件,假如文件中已经指明具体版本的依赖库,那么它会自动跳过计算版本号的过程,直接使用文件中的指明版本。简单点说,就是在 Cargo.lock 文件帮助下,当前的项目会一直使用 0.3.14 版本的 rand 包,直到我们手动升级至其他版本。

打个比方,假如我们使用的 rand 包在下周要发布 0.3.15 版本,它修复了一个 BUG,但这个修复会破坏我们现有的代码,这时,重新构建项目会发生什么呢?思考一下。

升级依赖包

当你确实想升级某个依赖包时, Cargo 提供一个专用命令:cargo update。它会强制 Cargo 忽略 Cargo.lock 文件,并重新计算出所有依赖包中符合 Cargo.toml 声明的最新版本。假如命令执行成功,Cargo 就会将更新后的版本号写入到 Cargo.lock 文件,并覆盖之前的内容。

另外需要注意一点就是 Cargo 在自动升级时只会寻找大于 0.3.0 并小于 0.4.0 的最新版本,这意味着当 rand 包发布了两个新版本:0.3.15 和 0.4.0 时,使用 cargo update 只会将当前的 0.3.14 升级到 0.3.15,而不会升级到 0.4.0。如果要升级到 0.4.0,需要去修改 Cargo.toml 文件。

5.1 生成一个随机数

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::Rng 中的 Rng 是一个 trait,它定义了随机数生成器需要实现的方法集合。gen_range 方法生成的数包含上限不包含下限,所以这里只产生 1 到 100 之间的随机数。

image

5.2 比较猜测数字与保密数字

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

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

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

新增了 Ordering 类型,它是一个枚举类型,有 Less、Greater 及 Equal 三种枚举成员,分别用来表示比较两个数之后可能产生的结果。后面使用 match 表达式来进行比较,有点类似 C 中 switch 结构,根据比较的结果与分支去比较,有 C 的基础就把它看做是 switch 语句,上述代码还不能通过编译,原因为字符串类型和数字类型的数据无法进行比较,编译会报错,改进一下:

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

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

    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!"),
    }
}

在这里创建了一个新的变量 guess,Rust 允许使用同名的新变量 guess 来隐藏旧变量的值,这一特性通常被用在需要转换值类型的场景中。

trim 方法会返回一个去掉了首尾所有空白字符的新字符串实例,之所以需要额外调用 trim 方法,是因为 u32 类型只能通过数字字符转换而来,用户在输入过程中敲击回车键会导致获得的输入字符串多出一个换行符,所以需要去掉,parse 方法会尝试将当前的字符串解析为某种数值,guess 后面的 : 告诉 Rust 我们将手动指定当前变量的类型,u32 表示的是 32 位无符号整型,如果用户随意乱输字符,可能会使得 parse 方法引发错误,所以需要 expect 来处理失败情况。

image

5.3 使用循环改进小游戏

在 Rust 中,loop 关键字会创建一个无限循环

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

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

    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 = 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!");
                break
            }
        }
    }
}

直到用户猜中并退出

image

是不是还算比较不错?

5.4 处理非法输入

为了进一步改善小游戏的可玩性,可以在用户输入一个非数字数据进简单地忽略这次猜测行为,并使用户可以继续进行猜测,从而避免程序发生崩溃。

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

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

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

Err 中的 _ 是一个通配符,可以匹配所有可能的 Err 值,而不管其中究竟有何种错误信息。在最终发布这个小游戏时记得把生成保密数字代码删除哦。

image

posted @ 2023-09-05 10:57  煙沫凡塵  阅读(108)  评论(0编辑  收藏  举报