学Rust,进华为!通过例子学习Rust语言:使用Rust编写猜大小游戏

查理谈科技 2024-05-10 00:00:06

学Rust, 进华为!

华为是Rust语言基金会的白金会员, 华为也对Rust的发展给出了大量的支持, 在华为内部的大量项目,都使用Rust 进行开发, 更是选定了Rust 作为华为可信计算的编程语言!

学习Rust 语言, 进华为工作!

今天我们接着来学习Rust 语言,通过一个猜数字大小的游戏, 来学习Rust 语言的命令行的基本输入输出功能。

一、程序功能

今天编写的Rust程序,将生成一个 1 到 100 之间的随机整数。然后它会在命令行里,提示玩家输入一个数字作为猜测值; 在接收到猜测值之后,程序将指示猜测是否太低或太高。 如果猜测正确,游戏将打印一条祝贺消息并退出。

二、程序实现

使用cargo创建新项目

要创建新项目,请在项目目录,并使用 Cargo 创建一个新项目,如下所示:

$ cargo new guessing_game$ cd guessing_game$ code .

执行结果:

cargo new guessing_game

第一个命令 Cargo new 将项目名称 (guessing_game) 作为第一个参数。

第二个命令将当前目录更改为新项目的目录。

第三个命令使用VS Code打开 这个目录, 我们在VS Code 中编写Rust代码。

查看生成的Cargo.toml文件:

[package]name = "learn_rust_guessing_game"version = "0.1.0"edition = "202401"# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html[dependencies]

注意⚠️:我们在这里把edition 的值(2021)改为当前的月份“202401”。

在上一个例子里我们讲过,cargo new 生成一个“Hello, world!” 的代码。 查看 src/main.rs 文件:

文件名:src/main.rs

fn main() { println!("Hello, world!");}

现在作为回顾,我们先来运行一下这个例子:

$ cargo run

查看结果:

guessing_game % cargo runerror: failed to parse manifest at `/Users/Think/Documents/dev/learn_rust/guessing_game/Cargo.toml`Caused by:failed to parse the `edition` keyCaused by:supported edition values are `2015`, `2018`, `2021`, or `2024`, but `202401` is unknown

可以看到, 编译出现了错误, 错误原因就是这个我们上面改变的选项edition, edition 的值仅可以是“2015”、“2018”、“2021”或者“2024”。

那么edition到底有什么作用呢? 为什么要用这么几个固定的值?

cargo.toml中edition的作用

原来,自从2015 年 5 月,Rust 1.0 发布之日起, Rust 就将“稳定而不停滞(stability without stagnation")”确立为 Rust 的核心原则,这个原则的用意,就是确保Rust 语言在保持稳定的同时,不断进步,增加新的特性。 具体表现在,自 1.0 版本以来,Rust 一旦某个功能在稳定版上发布,Rust社区就会致力于在所有未来版本中都支持该功能。

虽然保持兼容性很重要,但在实际情况里,有时候对不向后兼容的语言进行小的更改,这也是有必要、有价值的。 最明显的例子是引入一个新的关键字,这势必会使同名的变量无效。 例如,Rust 的第一个版本没有 async 和await 关键字。 在更高版本中突然将这些单词更改为关键字会破坏代码,例如 let async = 1;。

cargo的edition选项就是Rust语言用来解决这个问题的机制。 当Rust 想要发布一个向后不兼容的功能时,就需要把这个功能作为新 Rust 版本的一部分。 edition是选择性的,因此现有的 crate,如果在不更改edition的前提下, 在明确迁移到新版本的Rust语言,之前的代码也不会看到这些更改。

具体而言,这意味着,还是以async为例子,即使是最新版本的 Rust 仍然不会将 async 视为关键字,除非选择 2018 或更高版本。 此选择是根据每个crate的Cargo.toml来确定的。

[package]name = "learn_rust_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]

再次运行cargo run, 这次得到了正确的结果:

OK, 下面我们将在main.rs 里面继续编写我们的猜数字游戏。

开始编写猜数字游戏

猜谜游戏这个Rust程序的第一部分,就是要用Rust来显示一段话,要求用户输入数字,然后处理该输入,并检查输入是否为预期形式。 首先,我们将允许玩家输入猜测。 将清单 2-1 中的代码输入到 src/main.rs 中。

文件名:src/main.rs

use std::io;fn main() { println!("猜数字游戏!"); println!("请输入您的猜测数字。"); let mut guess = String::new(); io::stdin() .read_line(&mut guess) .expect("读取数字失败!"); println!("您的猜测数字是:{guess}");}

先来运行一下代码:

代码运行正常。

下面来逐行讲解这些代码。

为了获取用户输入然后将结果打印为输出,我们需要将 io 输入/输出库纳入范围。 io 库来自标准库,称为 std:

use std::io;

解释一下什么是Rust的预导入(preclude)

默认情况下,Rust 在标准库中定义了一组项目,并将其引入每个程序的范围内。 这套称为预导入(preclude)

下面是Rust文档中的预导入部分:

如果我们要使用的类型不在预导入中,则必须使用 use 语句显式将该类型带入作用域。 std::io 库提供了输入输出的功能,包括接受用户输入的部分。

跟C语言、Java 语言一样,main 函数是程序的入口点:

fn main() {

println!部分的语句就不用提了,之前的Rust文章已经介绍过了, 这里看看在Rust中,如何使用变量(variable)存储值。

首先,创建一个变量来存储用户输入,如下所示:

let mut guess = String::new();

使用let语句来创建变量。 在 Rust 中,变量默认是不可变的,这意味着一旦给变量赋予一个值,该值就不会改变。 为了使变量可变,我们在变量名前添加 mut。

关于Rust 变量默认是不可变量, 这一点其实恰恰和其他语言相反。 例如在C语言或者Java 语言中, 变量默认都是可变的, 如果想创建不可变的变量, 在Java中是通过const 来实现的。作为一门追求内存安全的语言, Rust 直接默认都是不可变的,这是Rust 语言的不同之处。

回到猜大小游戏程序,我们现在知道 let mut Guess 将引入一个名为 Guess 的可变变量。

String::new()的作用是一个返回 String 的新实例。 String 是标准库提供的字符串类型,它是可增长的 UTF-8 编码文本位。

“::new ”行中的 “::” 语法表明 new 是 String 类型的关联函数。 关联函数是在类型(本例中为 String)上实现的函数。 这个新函数创建一个新的空字符串。 您会在许多类型上找到新函数,因为它是产生某种新值的函数的通用名称。

总之,let mut Guess = String::new(); 这一行,创建了一个字符类型的可变变量。

Rust接受用户输入

下面这一行中,从 io 模块调用 stdin 函数,用行读入的模式,接受的用户输入:

io::stdin() .read_line(&mut guess)

io::stdin 函数返回 std::io::Stdin 的实例,这是Rust中的终端标准输入句柄, 类似于Java 语言中的System.in。

接下来,.read_line(&mut Guess) 调用标准输入句柄的 read_line 方法, 获取用户的输入,并且传给参数&mut suggest。

read_line 函数将用户输入到标准输入中的所有内容追加到参数指定的字符串,注意这里是追加,而不是覆盖字符串的内容!

& 表示该参数是一个引用,这对于C语言开发者而言是非常容易理解的, 它为开发者提供了一种让代码的多个部分访问一条数据,而无需多次将该数据复制到内存的方法。

引用是一项复杂的功能,Rust 的主要优点之一是使用引用的安全性和易用性。 现在,我们仅仅需要知道,与变量一样,默认情况下引用也是不可变的。 因此,我们在代码中才需要用“ &mut Guess ”而不是 “&Guess ”。

处理Exception

继续看代码:

.expect("读取数字失败!");

其实,这一行代码可以写成一行,就是这样:

io::stdin().read_line(&mut guess).expect("读取数字失败!");

但是,一行太长的代码很难阅读,因此最好引入换行符和其他空格来帮助分成多行,这也是编程的最佳实践之一。 而且, 在现在的IDE中, 代码在多个方法调用的时候, IDE还会给出对应的方法提示, 有助于更加清晰的了解当前调用对象和方法。

就像上面的截图中展现出来的,我们在通过read_line 将用户输入的内容放入字符串,VS Code 也清晰的显示出, readline返回来一个 Result 对象。这就是使用每行一个方法的好处!

Result对象是一个枚举(enumeration),有多种可能状态。 每种可能的状态称为变体(variant)。

Result实例的variant是 Ok 和 Err。 Ok变量表示操作成功,内容是成功生成的值。 Err 变体表示操作失败,Err 变体包含失败原因的信息。

Result 类型的值也定义了方法。 Result 的实例有一个 Expect 方法。 如果 Result 的此实例是 Err 值,expect 将导致程序崩溃并显示您作为参数传递给expect 的消息。

在本例子中, 如果 Result 的这个实例是 Ok 值,expect 将获取 Ok 所保存的返回值并将该值返回。

抑制警告的正确方法是实际编写错误处理代码,但在现在的例子中,为了简单起见,在出现错误时,这个程序退出即可,因此在这里不会有更详细的处理错误的情况。

来继续看代码。

使用 println! 打印值 占位符

println!("您的猜测数字是:{guess}");

此行打印现在包含用户输入的字符串。 大括号 {} 类似于一个表达式语言,在大括号内可以直接读取变量guess 的内容。

例如, 下面的例子展现了使用大括号{}进行简单的计算:

let x = 12; let y = 5;println!("{x} + {y} ={}",x+y);

结果:

程序解释完了,下面我们要为我们的代码添加一个新的功能, 产生随机数。有了这个随机数之后,我们才能让用户去猜测和调整输入。

引入随机数字产生器rand crate

我们需要生成一个随机数。 每次程序运行的时候,猜测的数字都应该不同,这样才有可玩性。 我们将使用 1 到 100 之间的随机数,这样游戏就不会太困难。

Rust 的标准库中尚未包含随机数功能(有点落后了)。 然而,Rust 团队提供了具有上述功能的 rand crate。

之前在上一篇文章中,我们介绍过,crate是 Rust 源代码文件的集合,可以在https://crates.io/中查看。

我们通过cargo run构建的项目是一个二进制包,它是一个可执行文件。 rand crate 是一个crate库,其中包含旨在在其他程序中使用,但是不能单独运行的代码。

多说一句:

Cargo 与外部crate 仓库的协调是 Cargo 的亮点之一,内置的Crate库也是Rust语言比C/C++等语言更为现代的表现。

在使用 rand 的代码之前, 在Cargo.toml 文件添加 rand 0.8.5作为依赖项:

[dependencies]rand = "0.8.5"

重新编译运行:

$ cargo run

当我们再次运行时, 可以看到rand 已经被编译到我们的代码中:

添加随机数代码

有了rand crate 之后, 我们在代码中产生一个随机数:

use std::io;use rand::Rng;fn main() { println!("猜数字游戏!"); //产生随机数 let magic_number = rand::thread_rng() .gen_range(1..=100); println!("请输入您的猜测数字。"); let mut guess = String::new(); io::stdin() .read_line(&mut guess) .expect("读取数字失败!"); //把两个数字都展现给用户 println!("您的猜测数字是:{guess},神秘数字是{magic_number}");}

现在再运行一次, 可以看到, 已经可以显示两个数字了, 一个是用户输入的数字,另一个是自动产生的神秘数字。

比较两个数字大小

从上面的代码来看, 我们已经能够产生随机数了, 但是,我们还没有比较这两个数字的大小, 也没办法告诉用户是否猜对了, 因此,我们需要在现有的代码上继续添加比较逻辑。

Rust 代码中,我们当然可以采用if else 结构, 但是Rust 提供了一个更好的match 方法, 来比较两个数字的大小:

let guess_number : u32 = guess.trim().parse().expect("请输入有效数字!"); if guess_number == magic_number { println!("恭喜您,您猜对了!"); } else{ println!("噢,您没有猜对!") }

注意, 这里需要增加一个新的变量guess_number, 这是因为, 在之前的读取用户输入的代码中, guess 变量是String 类型, 而read_line 方法接受的参数也是String 类型,也就是说, 我们得到的用户输入的数字类型实际上是String类型, 因此需要转换一下类型。

这里我们就使用一个新的数字类型的变量guess_number来存储这个用户输入的数字, 注意在对guess进行类型转换时, 可以先用trim方法来去掉字符串中的空白字符。

注意⚠️, 在Rust中,数字类型分为整型数字和浮点型数字, 而整数类型又可以分为有符号整数类型和无符号整数类型,我们这里guess_number用的是u8 类型,因为我们的这个程序,设定用户猜测的数字在1~100之内。,

到现在为止,猜数字大小的代码为:

use std::{io};use rand::Rng;fn main() { println!("猜数字游戏!"); //产生随机数 let magic_number = rand::thread_rng() .gen_range(1..=100); println!("请输入您的猜测数字(1~100)。"); let mut guess = String::new(); io::stdin() .read_line(&mut guess) .expect("读取数字失败!"); //展现两个数字 println!("您的猜测数字是:{guess}神秘数字是{magic_number}"); let guess_number : u8 = guess.trim().parse().expect("请输入有效数字!"); if guess_number == magic_number { println!("恭喜您,您猜对了!"); } else{ println!("噢,您没有猜对!") }}

运行一下, 看看结果:

允许用户多次猜测

在上面的代码中, 用户只能猜测一次, 猜对了或者猜错了程序都会终止。 现在我们再修改代码,允许用户可以多次输入, 直到用户猜对为止。

Rust 提供了循环表达式, 一共有四种:

loop 表达式表示一个无限循环。while 表达式不断循环,直到谓词为假。while let 表达式循环测试给定模式。for 表达式从迭代器中循环取值,直到迭代器为空。

这里我们用最简单的loop 循环表达式, 这个loop 可以无限循环,如果需要退出,则调用break 语句:

loop { println!("I live."); }

我们把之前的代码修改如下:

use std::{io};use rand::Rng;fn main() { println!("猜数字游戏!"); //产生随机数 let magic_number = rand::thread_rng() .gen_range(1..=100); loop { println!("请输入您的猜测数字(1~100)。"); let mut guess = String::new(); io::stdin() .read_line(&mut guess) .expect("读取数字失败!"); //展现两个数字 println!("您的猜测数字是:{guess}"); let guess_number : u8 = guess.trim() .parse() .expect("请输入有效数字!"); //不仅要是否相等,还要比较大还是小,给用户提示信息 if guess_number == magic_number { println!("恭喜您,您猜对了!"); break; } else if guess_number > magic_number{ println!("您输入的数字大了,请改小。") } else { println!("您输入的数字小了,请改大。") } }//end loop}

注意,我们把用户输入部分的代码, 以及比较大小的代码都挪到了loop 模块, 而且还修改了比较大小的逻辑, 当用户输入的数字和神秘数字完全相等时, 使用break 退出loop 循环。

使用cargo run, 运行效果如下:

代码运行成功!

总结

在这个例子里,我们做了一个猜测数字大小的游戏,综合使用了Rust的io库、数字类型转换、loop循环表达式等, 为了简单起见,并未对用户输入的非法字符进行检验, 后续会逐步添加这些细节部分。

本文所提及代码,已经提交到github, 网址为:https://github.com/hintcnuie/learn_rust/

如有需要,请下载相应代码。

0 阅读:0

查理谈科技

简介:感谢大家的关注