1. 上篇补充
在项目 hello_world 中,有一些文件。这里提一下每个文件的用途,了解一下即可,暂时不用深究,后面用到会详细讨论。
1. src :这个文件夹里主要用于存放源代码文件。Rust 项目的源代码文件通常以 .rs 为后缀,这些文件被组织在 src 文件夹中,以便于管理和维护。我们的 main.rs 文件就是在这里。
2. target :在 Rust 项目中,target 文件夹是用于存放编译时产生的中间结果和可执行文件的。这个文件夹由 Cargo 自己管理,一般情况下我们不需要过多关注。
3. .gitignore :这个文件用于配置 Git 版本控制系统忽略特定的文件和文件夹。它是一个纯文本文件,其中每一行都是一个模式,用于匹配要忽略的文件或文件夹。通过 .gitignore 文件,可以管理哪些文件不上传到版本管理服务中去。这个暂时不会用到,不做过多讨论。
4. Cargo.lock :该文件是 Cargo 工具根据同一项目的 TOML 文件生成的项目依赖的详细清单。这个文件记录了项目所有依赖的实际版本,包括库的版本、操作系统、架构等。因此,它通常不用修改。使用 Cargo 可以很方便地构建代码、下载依赖库、测试代码等,所以大多数情况下,推荐使用 Cargo 来构建项目。
5. Cargo.toml :它是 Rust 项目的配置文件,用于描述项目的元信息、依赖关系、编译选项等,是 Cargo 工具特有的项目数据描述文件,由开发者手动编写。Cargo.toml 文件通常包含2个方面的内容,一个是 package 部分:表示该项目的一些信息,其中 edition 字段指定编译的版本,缺省情况下默认是 2015。第二个是 dependencies 部分:表示需要依赖的一些外部的包。
Cargo.toml 文件是项目构建和编译的基础,如果 Rust 开发者希望项目能够按照期望的方式进行构建、测试和运行,则必须按照合理的方式构建 Cargo.toml 文件。
2. 让玩家输入一个数并打印出来
游戏目标:
生成一个1到100之间的随机数让玩家去猜,玩家猜的数比随机数大我们就提示他猜大了,比随机数小就提示他猜小了,他可以无限次数猜测下去直到猜对,然后我们就给一个他猜对了的提示并退出程序。
这个猜数游戏非常简单,但涉及的知识点很多,我们一步步来实现吧!
还记得怎么创建1个新项目吗?
在放置练习文件的目录下,打开黑窗口,输入 cargo new guessing_game 。我们给这个猜数游戏的项目起的名称就叫 guessing_name,cargo new 就是创建新项目的命令,后面跟的就是你给项目起的名称。
按回车键后,我们发现练习文件目录下又多了个名为 guessing_game 的文件夹,这个项目就创建好啦!
接着我们点进这个文件夹,再次打开黑窗口,输入 code . 直接在编译器中打开这个项目,然后在左边菜单栏中点开 src ,找到 main.rs 单击打开它,然后就可以写代码啦!
写什么呢?这一小节的目标是,获取玩家输入一个数字并打印出来。
use std::io;
fn main() {
println!("欢迎进入猜数游戏!");
println!("请输入你要猜测的数字:");
let mut guess = String::new();
io::stdin().read_line(&mut guess).expect("读取失败!");
println!("你猜测的数字为:{guess}");
}
代码已经写好啦!但是好像看不太懂?别急,下面我会把每一行代码讲解给你。
1. use std::io;
use 这个关键字就是导入第三方库的意思,类似 Python 中的 import 、C++ 中的 #include 。标准库(std)是所有标准库功能的默认入口点。io 模块是标准库中的一个模块,它提供了用于输入、输出操作的功能,这里我们要获取玩家猜测的数字,所以要用到这个模块。
2. fn main( ) { }
这个是一个特殊的函数,被称为程序的入口点。当你运行一个 Rust 程序时,main 函数是第一个被调用的函数。它不必有任何参数,并且总是返回一个 i32 类型的值,该值被解释为程序的退出码。什么是 i32 类型?这个后面会讲到。
3. println!("欢迎进入猜数游戏!");
println!("请输入你要猜测的数字:");
这2行就是分别打印2条信息,提示玩家已经进入游戏了,可以开始玩了。println! 是一个宏,它会在打印输出的时候自动在末尾加上一个换行符。输出效果如下图所示:
所以我们也可以把代码写成下面这样,显示的效果是一样的,\n 换行符在 Rust 中同样适用。
println!("欢迎进入猜数游戏!\n请输入你要猜测的数字:");
4. let mut guess = String::new();
我们先看等号右边部分 String::new(),这是调用 String 结构的静态方法 new(),它会创建一个新的、空的 String 实例。建好了给谁?就是等号左边名为 guess 的变量。
let 关键字用于声明一个不可变变量,而 mut 关键字用于声明一个可变变量。但在这里,由于我们想要一个可变的 String,所以我们又在中间加了个 mut ,因为玩家输入的内容并不是固定不变的。
所以,这行代码的整体意思就是,声明一个可变的空字符串变量,用于后面接收玩家的输入。
5. io::stdin().read_line(&mut guess).expect("读取失败!");
这行代码就是读取玩家的输入并传递给变量 guess 。
io::stdin():表示调用 io 模块中的 stdin 函数,它返回一个 Stdin 对象,该对象代表标准输入流。
read_line():这是 Stdin 对象的一个方法,用于从标准输入读取一行文本。这个方法会读取直到遇到换行符(\n),并将该行内容作为一个字符串返回。
&mut guess:这是一个可变引用,指向我们之前声明的 guess 变量。我们使用 &mut 是因为我们需要修改 guess 的内容。
expect("读取失败!"):这是对 read_line 方法的结果进行错误处理的简短方式。如果 read_line 成功,那么它的返回值是 Ok(value),其中 value 是读取到的字符串。如果发生错误,它会返回 Err(e),e 是错误类型。使用 expect 方法会将任何错误转换为 panic(注:中文是恐慌的意思),并显示提供的消息:“读取失败!”。
这个读取失败不是固定写法,你可以在两个双引号之间随便写你想让它在读取失败后显示什么内容。
6. println!("你猜测的数字为:{guess}");
这行代码也是打印的意思,但是不同于上面的打印,这里多了个 {guess} ,是使用了字符串格式化,通过 { } 花括号 插入了变量 guess 的值。
你可以把这对花括号理解为占位符,所以这行代码也可以写成下面这样,效果是一样的。
println!("你猜测的数字为:{}", guess);
在黑窗口中输入 cargo run 命令运行一下,并输入数字 10:
欧克!木有问题,非常奶思~
3. 生成随机数
这一小节,我们让程序生成一个随机数,然后打印出来。代码如下:
use std::io;
use rand::Rng;
fn main() {
println!("欢迎进入猜数游戏!\n请输入你要猜测的数字:");
let random_number = rand::thread_rng().gen_range(1..101);
println!("这个神秘数字是:{random_number}");
let mut guess = String::new();
io::stdin().read_line(&mut guess).expect("读取失败!");
println!("你猜测的数字为:{}", guess);
}
第二行代码 use rand::Rng ,我使用 use 关键字导入了 rand 库中的 Rng trait,用于接下来在函数体中生成随机数。这里的 rand 是一个 Rust 标准库中的随机数生成器库,而 Rng 是这个库中的一个 trait,代表 “随机数生成器”。
那这个 trait 是个什么东东?它的中文意思是 特征 。在 Rust中,它是一个核心概念,用于定义对象、函数或数据结构的通用行为。
类似于其他面向对象编程语言中的接口或者抽象类。Rust 中的 trait 是一种泛型类型,意味着它定义了一种可以在多种类型上工作的抽象规范。
听起来有点晦涩是不是?没有关系,咱们以后再细讲。这里你只需要知道,我们要得到一个随机数,要先导入 rand 库中的随机数生成器 Rng 就行了。
然后我们再讲一下中间新增的2行代码:
let random_number = rand::thread_rng().gen_range(1..101);
println!("这个神秘数字是:{random_number}");
第一行代码的总体意思是:在当前线程中,生成一个1到100之间的随机整数,并将其存储在变量 random_number 中。
首先,rand::thread_rng():这是 Rust 标准库中的 rand 模块提供的一个函数,用于获取当前线程的默认随机数生成器。每个线程都有自己的随机数生成器,这样可以避免多线程环境下的竞争条件。
gen_range(1..101):这是 rand 模块中 Rng trait 的一个方法,用于生成一个指定范围内的随机数。这里,它生成一个从 1 到 101 之间的随机整数,包括1但不包括101。
let random_number:声明一个名为 random_number 的变量,用于接收等号右边生成的随机数。
然后第二句代码就是把这个随机数打印出来。
这里需要提一下,我们使用这个随机数生成器不是只要在 .rs 文件中用 use 导入一下就行的,还需要打开 Cargo.toml 文件,在 dependencies 下指定你要导入的 rand 的版本号。如下图所示:
指定 rand 的版本后,需要先用 cargo build 命令编译一下项目,然后在 .rs 文件中才能正常使用哦!
编译后,我们点开 Cargo.lock 文件,发现 rand 包的版本号是 0.8.5,但实际我们写的是 0.8.0 。 这是为什么呢?
Rust 的包管理工具 Cargo 在处理包的版本升级方面具有自动化和灵活的特性。Cargo 从 crate.io 下载依赖包,并允许通过 TOML 文件中的指定版本来限制包的版本。
默认情况下,只需要在 TOML 文件的 dependencies 部分提供包名和版本号,例如 [dependencies] time = "0.1.12" 。这里,"0.1.12" 是一个 semver 格式的的版本号,符合 "x.y.z" 的形式,其中 x 被称为主版本(major),y 被称为小版本(minor),而 z 被称为补丁版本(patch)。
这种版本号表示法的特点是,从左到右,版本的影响范围逐步降低,补丁的更新通常无关痛痒,并不会造成 API 的兼容性被破坏。这也是上面为什么 Cargo 会自动将我们指定的版本的小版本号给篡改了,因为这并不会影响什么。
另外,Cargo 还支持使用 ^ 符号来指定一个版本范围。例如,"^0.1.12" 表示使用 0.1.x 系列中的最新版本,只要新的版本号没有修改最左边的非零数字,即1,那么它就再允许的版本号范围中。这意味着,如果有一个新的版本 0.1.13 ,Cargo 会自动将其引入作为依赖包,因为它仍然在允许的版本范围内。
这种机制允许开发者依赖于特定版本的库,同时又能享受到新版本的修复和改进。当开发者需要更新依赖包时,他们可以通过更新 TOML 文件中的版本号来主动升级,或者让 Cargo 自动处理版本升级以满足依赖关系。
综上所述,Rust 的 Cargo 工具允许通过指定具体的版本号或使用版本范围来控制包的依赖关系,包括包的自动升级。这样做有助于确保项目的稳定性和兼容性,同时也能够利用最新的库功能和修复。
有时候我们会忘记先在 TOML 文件中指定包的版本号,怎么办呢?有个非常简单的办法就是,当你使用 use 关键字导入包之后,在写具体代码时,发现这个包里的函数没有出现代码补全提示,这就说明你还没有给这个包指定版本。
4. 将猜测的数和随机数比较
我们需要先将玩家猜测的数字和系统生成的随机数比较大小,才能正确提示玩家是猜大了还是猜小了,如果相等,那就提示他猜对了,然后程序退出。
完整代码如下:
use std::io;
use rand::Rng;
use std::cmp;
fn main() {
println!("欢迎进入猜数游戏!\n请输入你猜测的数字:");
let random_number = rand::thread_rng().gen_range(0..101);
loop {
let mut guess = String::new();
io::stdin().read_line(&mut guess).expect("读取失败!");
let guess:u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
match guess.cmp(&random_number) {
cmp::Ordering::Less => {
println!("你猜的数小了,请再猜测一次:");
continue;
},
cmp::Ordering::Greater => {
println!("你猜的数大了,请再猜测一次:");
continue;
},
cmp::Ordering::Equal => {
println!("恭喜,你猜对啦!");
break;
},
}
}
}
与之前代码不同的是,我把大部分代码放在了 loop 后面的花括号中。在 Rust 中,loop 关键字用于创建一个无限循环。当执行到 loop 时,程序会一直执行循环体内的代码,直到遇到 break 语句或者外部干预(如操作系统终止程序,注:Ctrl + C 可手动终止黑窗口中的死循环)。
因此,当玩家猜测的数和随机数相等时,我们就用 break 来终止该循环,反之就用 continue 循环继续,直到玩家猜测正确为止。
在代码顶部,我们又导入了一个库 cmp ,它是一个用于比较两个值的函数,属于 std::cmp 模块,该模块提供了各种比较函数,如:cmp::Ord 和 cmp::TotalOrd 等。
cmp 函数比较两个值大小关系后返回一个比较结果,我们可以根据比较结果来决定执行哪些操作。比如在我们这个猜数游戏程序中,是将玩家猜测的数字和随机数进行对比,如果猜测的数字比随机数大,那就打印提示大了,让玩家继续猜,如果比随机数小,那就打印提示小了,也让玩家继续猜,如果相等,那么就打印恭喜猜对并退出程序。
所以,这里会产生3种比较结果,大于(cmp::Ordering::Greater =>)、小于(cmp::Ordering::Less =>)和相等(cmp::Ordering::Equal =>)。=> 组合符号后面跟的是对应的操作。
在比较的代码之前还有如下几行代码:
let guess:u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
这几行代码是什么意思呢?在此之前,我们获取玩家猜测的数字并赋给变量 guess ,并且我们给这个变量还创建了一个实例,它是字符串类型的。 而 cmp 是将两个值进行比较,字符串和整数(1到100之间的随机数)是无法进行比较,这样程序就会报错。
所以,我们在比较它们之前,还需要先将 guess 的值转换成整数类型才行。trim 是对字符串进行修剪,即:移除前后的空白字符。parse 是尝试将这个字符串解析为一个 32位无符号整数,即 u32 。
为什么 parse 会知道我们要转换的目标类型是 u32 呢?因为这行代码的等号左边,我们已经显式声明啦!即:guess:u32 。
代码写完了,我们来运行一下吧!
2次竟然就猜对了?!
这不会消耗了一些我这辈子本就不多的运气吧?
我o(╥﹏╥)o了,读者大大们给个赞安慰一下角角吧 ~
5. 问题补充
有些读者大大可能和角角有一个同样的困惑,那就是如何判断一个库是否要在 TOML 文件中声明?貌似有的需要声明,有的又不需要?
下面就这个疑问详细解答一下吧!
在 Rust 中,判断一个库是否需要在 Cargo.toml 文件中声明主要基于以下几点:
5.1 外部依赖库:
如果你正在使用一个外部的、非 Rust 标准库的库,那么你通常需要在 Cargo.toml 文件中声明它。这是因为外部库可能需要被编译和链接到你的项目中。例如,你可能使用了某个特定的数学库、网络库或 GUI 库。
5.2 内联依赖:
对于一些内联的、非外部的库,可能不需要在 Cargo.toml 文件中声明。这些通常是项目内部的库或是 Rust 标准库的一部分。例如,Rust 标准库就包含许多常用功能,如:std::collections 等,这些通常不需要在 Cargo.toml 中额外声明。
5.3 crate 类型:
在 Rust 中,一个 crate 可以是一个库(lib)或一个二进制(bin)。如果你正在创建一个库 crate(即:你的项目类型在 Cargo.toml 被设置为 lib),那么你可能需要声明所有的外部依赖。如果你正在创建一个二进制 crate,那么你可能只需要声明那些被直接使用的外部依赖。
5.4 构建脚本(build scripts)和 proc-macro:
某些情况可能需要特殊的构建脚本或 proc-macros。这些也需要在 Cargo.toml 中声明。
总的来说,判断是否需要再 Cargo.toml 中声明一个库,最关键的是看这个库是否需要被编译和链接到你的项目中。如果需要,那么你就需要在 Cargo.toml 中声明它。如果不需要,那么可能就不需要。
6. 结语
由于能力有限、本人也还在学习摸索阶段,文中难免有错漏之处,若有读者大大发现,欢迎在评论区留言。
最后,码字不易,即便只有一个赞也可以让我动力满满,感谢你的支持!