Rust 学习笔记:猜数游戏

Rust 学习笔记:猜数游戏

游戏规则

在这里插入图片描述

创建项目

在这里插入图片描述

编译、运行一下,没问题。

在这里插入图片描述

处理一次用户输入

猜谜游戏程序的第一部分将请求用户输入,处理该输入,并检查输入是否符合预期的形式。首先,我们将允许玩家输入猜测。

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库,io 库来自标准库 std。

main 函数是进入程序的入口点。fn 语法声明了一个新函数,括号 () 表示没有参数,而大括号 {} 则是函数体的开始。

println !是一个宏,将字符串打印到屏幕上。

在 Rust 中,变量在默认情况下是不可变的,这意味着一旦我们给了变量一个值,这个值就不会改变。要使变量可变,在变量名前加 mut。

等号的右边是 guess 绑定的值,它是调用 String::new 函数的结果,该函数返回 String 的新实例。String 是标准库提供的一种字符串类型。

从 io 模块调用 stdin 函数,这将允许我们处理用户输入。

如果没有使用 use std::io 导入 io 库,也可以通过将这个函数调用写成 std::io::stdin 来使用这个函数。

接下来,.read_line(&mut guess) 调用标准输入句柄上的 read_line 方法从用户获取输入。我们还将 &mut guess 作为参数传递给 read_line,告诉它在哪个字符串中存储用户输入。read_line 的全部工作是将用户键入的任何内容添加到标准输入中,并将其附加到字符串中(不覆盖其内容),因此我们将该字符串作为参数传递。字符串参数需要是可变的,这样方法就可以改变字符串的内容。

& 表示该参数是一个引用。引用是一个复杂的特性,可以让代码的多个部分访问同一段数据,而无需将该数据多次复制到内存中。与变量一样,引用在默认情况下是不可变的。因此,需要编写 &mut guess 而不是 &guess 以使其可变。

read_line 返回一个 Result 值,结果是一个枚举。其中,Ok 表示操作成功,它包含成功生成的值;Err 表示操作失败,它包含有关操作失败的方式或原因的信息。

在这里插入图片描述

Result 的实例有一个可以调用的 expect 方法。如果 Result 的这个实例是一个Err 值,expect 将导致程序崩溃,并显示作为参数传递给 expect 的消息。如果 read_line 方法返回 Err,则很可能是底层操作系统出错的结果。如果 Result 的这个实例是 Ok 值,expect 将获取 Ok 所持有的返回值,并将该值返回。在本例中,该值是用户输入中的字节数。

在 Rust 中,如果返回值是一个 Result,那必须处理一下这个值。
如果不加这个 except 函数,编译会失败并报错。

{} 是一个占位符,在打印变量值时,变量名可以放在花括号内。在打印表达式求值的结果时,在格式字符串中放置空花括号,然后在格式字符串后面以逗号分隔的表达式列表,以相同的顺序在每个空花括号占位符中打印。

比如:

let x = 5;
let y = 10;

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

这将打印 x = 5 and y + 2 = 12。

到这里就讲解完了,编译、运行一下目前的程序。

在这里插入图片描述

导入生成随机数的库

接下来,我们需要生成一个用户将尝试猜测的秘密数字。每次的秘密数字都应该是不同的,这样游戏才会更有趣。我们将使用 1 到 100 之间的随机数,这样游戏就不会太难。Rust 还没有在其标准库中包含随机数功能。然而,Rust 团队确实提供了一个具有上述功能的 rand crate。

rand crate 是一个库 crate,它包含了打算在其他程序中使用并且不能单独执行的代码。

添加库的第一种方法

在编写使用 rand 的代码之前,我们需要修改 Cargo,将 rand crate 作为依赖项包含在内。打开 Cargo.toml,并在 [dependencies] 部分标题下方添加以下行。

[dependencies]
rand = "0.8.5"

在这里插入图片描述

一定要使用正确的版本号,否则代码可能无法工作。

在 RustRover 中,会自动重新构建项目。

在这里插入图片描述

当然我们也可以手动构建。

在这里插入图片描述

在本例中,虽然我们只将 rand 列为依赖项,但 Cargo 还抓取了 rand 所依赖的其他 crate。下载完这些 crate 后,Rust 会编译它们,然后用可用的依赖项编译项目。

如果不做任何更改,再次运行 cargo build,那么除了 Finished 之外,将不会得到任何输出,因为 Cargo 知道它已经下载并编译了依赖项,只通过对 src/main 的微小更改来更新构建。

在这里插入图片描述

同理,在[dependencies] 部分中删除 rand,RustRover 也会触发自动构建。

添加库的第二种方法

先删掉之前的 rand,我们这次尝试用命令行添加。

cargo add rand@0.8.5

在这里插入图片描述

因为刚删没多久,是有缓存的,所以添加得非常快。

@0.8.5 的意思是指定了 0.8.5 版本,如果去掉,会添加最新版本的库。命令行中我们可以看到,库是从 crates.io 这个网站下载的。

Cargo.lock

当我们第一次构建项目时,就会创建 Cargo.lock 这个文件。Cargo 会找出符合标准的依赖项的所有版本,然后将它们写入 Cargo.lock。

里面存有所有的依赖项的版本和详细信息。

在这里插入图片描述

Lock 文件对于可复制的构建非常重要,它通常与项目中的其他代码一起签入源代码控制。

在将来构建项目时,Cargo 将看到锁文件存在,并将使用那里指定的版本,而不是再次执行所有找出版本的工作。这样能保证构建的可复制性。

更新 Crate 以获得新版本

Cargo 提供命令更新,它将忽略 Cargo.lock 文件,在 Cargo.toml 中找出所有符合依赖规格的最新版本。然后,重新写入 Cargo.lock 文件。

命令如下:

cargo update

在这里插入图片描述

创建依赖文档

Cargo 的另一个简洁特性是,运行 Cargo doc --open 命令将在本地构建所有依赖项提供的文档,并在浏览器中打开它。

在这里插入图片描述

在这里插入图片描述

所有 doc 在 target 文件夹下。

在这里插入图片描述

上面的网页在以项目名为名的文件夹(guessing_game)里的 index.html 中。

在这里插入图片描述

生成一个随机数

修改代码:

use std::io;
use rand::Rng;

fn main() {
    println!("Guess the number!");

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

    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 定义了随机数生成器实现的方法。

调用 rand::thread_rng 函数,该函数为我们提供了将要使用的特定随机数生成器。

在这里插入图片描述

这个生成器与当前线程有关,随机数种子由操作系统提供。

然后我们调用随机数生成器上的 gen_range 方法,该方法接受一个范围表达式作为参数,并在该范围内生成一个随机数。我们在这里使用的范围表达式的形式是 start…=end 并且包含下界和上界,因此我们需要指定 1…=100 请求 1 到 100 之间的数字。

运行结果:

在这里插入图片描述

将用户输入与随机数作比较

现在我们有用户输入和一个随机数,我们可以进行比较。

use std::cmp::Ordering;
use std::io;
use rand::Rng;

fn main() {
    println!("Guess the number!");

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

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

首先,我们添加另一个 use 语句,将一个名为 std::cmp::Ordering 的类型从标准库引入作用域。Ordering 类型是另一个枚举,具有 Less、Greater 和 Equal 三个变量。这是比较两个值时可能出现的三种结果。

cmp 方法比较两个值,可以在任何可以比较的对象上调用。它接受一个你想要比较的对象的引用:这里它比较的是 guess 和 secret_number。然后,它返回使用 use 语句引入范围的 Ordering 枚举的一个变体。我们使用匹配表达式根据调用 cmp 返回的 order 的哪个变体(包含 guess 和 secret_number 中的值)来决定下一步要做什么。

IDE 提醒我们:类型不匹配。

在这里插入图片描述

guess 是一个 String 类型,而 secret_number 是一个 i32,即有符号的 32 位数字。

将程序作为输入读取的 String 转换为数字类型,以便将其与秘密数字进行数值比较。添加代码:

let guess: u32 = guess.trim()
					  .parse()
				      .expect("Please type a number!");

Rust 允许我们用一个新值来遮蔽 guess 的前一个值,这让我们可以重用 guess 变量名,而不是强迫我们创建两个唯一的变量。

String 实例上的 trim 方法将消除开头和结尾的任何空白,在将字符串转换为 u32 之前必须这样做,u32 只能包含数字数据。

字符串的 parse 方法将字符串转换为另一种类型。这里,我们使用它将字符串转换为数字。我们需要通过 let guess 告诉 Rust 我们想要的确切数字类型:u32。guess 后面的冒号告诉 Rust 我们将注释变量的类型,这里看到的 u32 是一个无符号 32 位整数。

运行结果:

在这里插入图片描述

我们现在已经完成了大部分游戏,但用户只能做出一次猜测。让我们通过添加一个循环来改变它!

使用循环允许多次猜测

loop 关键字创建一个无限循环。

use std::cmp::Ordering;
use std::io;
use rand::Rng;

fn main() {
    println!("Guess the number!");

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

    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

Ordering::Equal => {
                println!("You win!");
                break;
            }

处理无效输入

为了进一步完善游戏的行为,而不是在用户输入非数字时使程序崩溃,让我们让游戏忽略非数字,以便用户可以继续猜测。我们可以通过修改将 guess 从 String 转换为 u32 的那一行来实现。

let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };

我们从 expect 调用切换到 match 表达式,从而从发生错误时崩溃转向处理错误。请记住,parse 返回一个 Result 类型,而 Result 是一个枚举,具有 Ok 和Err 变体。

如果 parse 能够成功地将字符串转换为数字,它将返回一个包含结果数字的 Ok 值。该 Ok 值将匹配 {} 的第一行,匹配表达式将返回 parse 生成的 num 值,并将其放入 Ok 值中。这个数字最终放入新创建的 guess 变量中。

如果 parse 不能将字符串转换为数字,它将返回一个 Err 值,其中包含有关错误的更多信息。下划线_是一个集合值。在这个例子中,我们说想要匹配所有Err值,不管它们里面有什么信息。因此程序将执行 continue,这告诉程序进入循环的下一次迭代,并要求进行另一次猜测。

最终程序

回想一下,程序仍然在打印密码。这对于测试来说非常有效,但却会破坏游戏。让我们删除 println!输出密码,或者注释掉。

use std::cmp::Ordering;
use std::io;
use rand::Rng;

fn main() {
    println!("Guess the number!");

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

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

运行结果:

在这里插入图片描述

总结

这个项目以一种动手的方式介绍了许多新的 Rust 概念:let、match、函数、外部 crate 的使用等等。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

UestcXiye

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值