Rust 入门实战系列(2)- Guessing Game 实战
今天我们跟着官方的实战案例 Guessing Game 来了解一些基础的 Rust 概念,上手操练起来。了解 Rust 中的 let
, match
, 方法,函数等基础用法。
场景
这是一个针对初学者的实战案例,原理很简单:一个程序将会随机生成一个 1 到 100 之间的数字,玩家需要进行输入自己猜测的数字。此时程序会告诉玩家这个猜测是高了,还是低了。然后玩家继续下一轮猜测,直到命中。此时,程序会打印【恭喜】的消息并结束。
创建新项目
这里我们还是会用此前给大家介绍过的 rust-learn 项目来记录源码,感兴趣的同学可以直接到 Github 仓库了解一下。
首先,我们还是用 Rust 的包管理工具 cargo 来创建今天的项目:
$ cargo new guessing_game
=======================================================
Created binary (application) `guessing_game` package
此时在根目录下会多出来一个 guessing_game
的子目录,我们 cd 进去。
还是熟悉的结构,里面是个默认的 Hello World。Cargo.toml 里面也是默认的内容,没有外部依赖:
[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]
这里如果我们执行 cargo run
还是会像此前那样输出 Hello, world!
$ cargo run
=====================================================
Compiling guessing_game v0.1.0 (/Users/ag9920/go/src/github.com/ag9920/rust-learn/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 1.83s
Running `target/debug/guessing_game`
Hello, world!
处理猜测的输入输出
要实现我们一开始说的场景,需要有以下几个条件:
- 生成 1 - 100 之内随机数;
- 获取用户输入的内容,校验格式;
- 对比两个变量是否相等;
- 输出提示信息到用户。
Rust 针对所有项目都会自带一些库,我们很多时候是不用手动来 use std::xxx
的。这个机制叫做 Prelude,我们可以通过这个链接看看最新版 Rust 支持了哪些 Prelude
变量
在 Rust 中,我们需要用 let
来做到这一点。例如
let apples = 5;
这里我们创建了个新的变量 apples,并且赋值为 5。需要注意的是,Rust 中的变量默认都是不可变的(immutable) 即一旦我们赋值给某个变量,此后这个值无法更改。如果我们希望更改,则需要在变量名称前,加上 mut
标识符。
let apples = 5; // 不可变
let mut bananas = 5; // 可变
mut 其实就是英语 mutation 的意思,表示变更。
那字符串应该怎么表示呢?
在 Rust 中同样有类似 Golang 的 strings 包,封装了对于字符串的操作,其实仔细看一下你会发现在我们上一节的 prelude 部分也包含了 string 包的导入:
std::string::{String, ToString}
, heap-allocated strings.
这里默认导入的 String 是一个【标准库提供的,可增长的,UTF-8 编码的字符串类型】。
我们可以用 String::new()
来调用 String 的 new()
成员函数,它能够创建一个新的,空字符串。
所以,我们想要用变量来存储用户输入,需要先声明一个字符串变量来承接,就可以用这样的代码来完成:
let mut guess = String::new();
此时的 guess 就是一个【可变的】,【绑定到一个新创建的空字符串实例的】对象。
输入输出
Rust 标准库中提供了 io 包用来承接输入/输出相关的能力。所以我们需要用 use std::io
这个格式将 io
包导入进来。因为 io 并不在 prelude 的范围内。
导入进来后,我们就可以用 io 包下的 stdin
函数来获取用户输入,连上此前我们介绍的声明字符串变量的方法,写出的代码会是这样:
use std::io;
fn main() {
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
}
当然,我们也可以不在一开始 use std::io;
导入,而是直接在需要用到的地方导入,这样会变成:
std::io::stdin().read_line(&mut guess)
stdin
函数返回了一个 std::io::Stdin 对象,它代表了一个命令行标准输入的句柄(handle),调用它的 read_line
方法就能够读取到用户的输入。我们传入了 &mut guess
参数,这样 read_line
就会把用户的输入存在 guess
变量中(注意,这里是 append 而不是覆盖写,传入的对象一定得是可变的,这样才能去追加内容)
& 和其他语言一样都代表了【指针】,需要注意的一点是,在 Rust 中,指针默认也是不可变的(immutable),我们在这里加上 mut 可不是因为 guess 是个可变的,还需要再次声明。而是因为我们希望这个指针也是可变的。用法是一样的,所以要用 &mut guess
。
read_line
函数的返回值是个 Result
,这是一个枚举值,他能够对错误处理的信息进行编码。Result
可能的枚举值有两个:Ok
, Err
。
Ok
代表这次操作正常,内部包含了生成的值,而 Err
则代表操作失败,内部包含了失败的原因。
在 Result
的成员方法中,有一个 expect
可以适配我们的场景。如果我们拿到的 Result
是个 Err
,expect
方法会导致程序挂掉并且展示我们传入的参数。如果 Result
是个 Ok
,那么 expect
同样会返回 Ok
持有的值。
简单来类比一下,有了 Result,就要求我们妥善进行错误处理。Ok 和 Err 本质就是成功了怎么办,失败了怎么办的问题。如果我们刚才只是简单的 io::stdin().read_line(&mut guess);
就结束,此时编译器能够通过,但会提醒我们对于 Result 应该妥善处理,这是个 warning。
所以,我们这里可以选择通过 expect
函数来打印一个错误信息。
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
让它挂掉就 ok,恢复也不是不可以,类似 Golang 中的 defer recover,但这不是我们目前的重点。暂时简单 expect 就 ok。
至此,我们已经可以从用户输入读到值,写入我们的 guess 变量中,那怎么打印回 console 呢?总要让用户看到自己输入的是什么吧,这样提示信息才算完整。
这个占位符的诉求其实 println!
已经支持,我们可以直接这样:
println!("You guessed: {guess}");
这里会按照 {}
中的字符串来匹配此前声明的变量。
而如果我们希望显式地传参,或者多个参数,这里类似 Python 的写法,也是支持的:
let x = 5;
let y = 10;
println!("x = {} and y = {}", x, y);
好了,目前为止我们学习了【变量】如何声明,以及如何从标准输入输出来获取,以及打印内容。
我们来更新一下 main.rs ,先能够做到拿到输入,打印输入:
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}");
}
我们来执行一下 cargo run
,结果如下:
$ cargo run
Compiling guessing_game v0.1.0 (/Users/ag9920/go/src/github.com/ag9920/rust-learn/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 1.15s
Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
9920
You guessed: 9920
完美符合预期,我们通过键盘输入 9920,这个结果也按照程序逻辑打印了出来。下面我们继续前进,完善随机数逻辑。
生成随机数
通过 crate 添加依赖
在 Rust 标准库中其实并不存在一个【随机数】的库。但是,但是,不要灰心。Rust 团队提供了一个 rand 的 crates 来承接这项能力。
好了,crates 又是个啥?
我们其实之前简单提过,crates 是一些 Rust 源码文件。我们今天要用的这个 rand 就是一个库,不能独立执行,可以作为其他项目的依赖。
我们来调整一下 Cargo.toml 看看怎么把 rand 给添加进来作为我们的依赖。
在 dependencies 这个部分,我们告诉 Cargo 希望把哪些外部的 crates 添加进来,以及他们的版本。这里我们就要求使用 0.8.3 的 rand(这个版本号会被理解成 >= 0.8.3 且 < 0.9.0,参照 SemVer 的规则)
此时我们保持 main.rs 先不动,触发一次编译看看。
$ cargo build
==================================
Updating crates.io index
Downloaded rand v0.8.5
Downloaded rand_chacha v0.3.1
Downloaded getrandom v0.2.7
Downloaded rand_core v0.6.3
Downloaded libc v0.2.132
Downloaded ppv-lite86 v0.2.16
Downloaded 6 crates (770.7 KB) in 2.53s
Compiling libc v0.2.132
Compiling cfg-if v1.0.0
Compiling ppv-lite86 v0.2.16
Compiling getrandom v0.2.7
Compiling rand_core v0.6.3
Compiling rand_chacha v0.3.1
Compiling rand v0.8.5
Compiling guessing_game v0.1.0 (/Users/ag9920/go/src/github.com/ag9920/rust-learn/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 2m 06s
可以看到,此时 Cargo 会自动从 Crates.io 拉取依赖(这是 Rust 的生态系统,开发者会把开源项目发布到 Crates.io),我们依赖的是 0.8.5 (这也说明了 SemVer 的语义,并不是写了 0.8.3 就一定会拉这个版本,只要算在同一个小版本里就 ok)
注意虽然我们只是加了一个 rand,但 Cargo 也会连带着把 rand 依赖的包也下下来。
随机数
现在我们有了 rand crates,它包含了 Rng
这个 trait。
Rust语言中的 trait 是非常重要的概念。在中文里,trait可以翻译为“特征”、“特点”、“特性”等。在Rust中,trait这一个概念承担了多种职责。比如成员方法,静态方法,扩展方法。trait本身不是固定大小的类型,它只是定义了针对类型的“约束”。不同的类型都可以实现同一个trait,满足同一个trait的类型可能具有不同的大小。因此,trait在编译阶段没有固定大小,我们不能直接使用trait当作实例变量、参数、返回值。
目前可以粗浅地把它和 Golang 中的 interface 类比。
Rng 这个 trait 里面包含了一组随机数相关的方法。所以,我们可以用 rand::thread_rng
来获取这一组随机数生成器,调用 gen_range
方法,输入一个取值范围,输出一个随机值。
let secret_number = rand::thread_rng().gen_range(1..=100);
这里我们用了 start...=end
的写法标识一个取值范围,这是闭区间。
比较猜测结果
现在我们可以生成随机数了,也能获取用户输入,下来就是作比较判断是否正确了。
这里我们需要用到标准库中的 cmp 包下的 Ordering,这也是个枚举值类型,支持 Less, Greater, Equal。
match
语句类似 Golang 中的 switch,支持多个分支,所以我们修改后的 main.rs 类似这样:
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
// --snip--
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!"),
}
}
但是注意,此时的代码可编译不了,因为 secret_number 是个 integer 整型,而我们的 guess 是个字符串,报错如下:
error[E0308]: mismatched types
--> src/main.rs:23:21
|
23 | match guess.cmp(&secret_number) {
| --- ^^^^^^^^^^^^^^ expected struct `String`, found integer
| |
| arguments to this function are incorrect
|
= note: expected reference `&String`
found reference `&{integer}`
note: associated function defined here
For more information about this error, try `rustc --explain E0308`.
error: could not compile `guessing_game` due to previous error
这里也可以看到 Rust 的编译器还是很贴心,位置,错误原因都说的很清楚。
所以,这里要求我们把 guess 转成整型再比较。我们可以这样做:
let guess: u32 = guess.trim().parse().expect("Please type a number!");
虽然我们此前已经有 guess 了,但是Rust是允许我们覆盖定义(shadow)的,这样我们就不用浪费一次新的定义,而是复用老的变量名就 ok。
trim
是 String 实例下的方法,他会去除前后的空格,parse 方法则能够从 string 转为 number。而我们一开始就声明了新的类型 u32,代表了 unsigned 32位整数,小整数的默认类型。
这样随后比较的时候, Rust 也会默认我们的 secret_number 也是 u32 类型,这样保证同一类型。
转化之后,我们的代码变成了这样:
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..=100);
println!("The secret number is: {secret_number}");
println!("Please input your guess.");
// --snip--
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!"),
}
}
再次运行,结果终于符合预期:
$ cargo run
=========================================================
Finished dev [unoptimized + debuginfo] target(s) in 0.08s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 93
Please input your guess.
99
You guessed: 99
Too big!
支持多次猜测
加循环的时候到了,总不能只支持用户猜一次吧。
这里我们可以使用 loop
语句来帮助我们完成,改一下代码:
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..=100);
// --snip--
println!("The secret number is: {secret_number}");
loop {
println!("Please input your guess.");
// --snip--
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!"),
}
}
}
此处的 loop 等价于 Golang 里面的无条件的 for,本质是个无限循环。我们当然也可以用 Ctrl + C 来退出,但是这样不太好。如果我都猜中了,为什么还要继续呢?
这可以配合 break
命令来用,跟其他语言其实是类似的。
当用户猜中了,我们可以直接打印胜利信息后,break 出来就自动退出了。
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
搞定,现在我们就有了一个完整的案例了,最终代码如下:
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..=100);
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;
}
}
}
}
我们来运行一下,结果如下:
$ cargo run
===========================================
Finished dev [unoptimized + debuginfo] target(s) in 0.08s
Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
50
You guessed: 50
Too small!
Please input your guess.
75
You guessed: 75
Too big!
Please input your guess.
68
You guessed: 68
Too big!
Please input your guess.
60
You guessed: 60
Too small!
Please input your guess.
64
You guessed: 64
You win!
完美,用二分法,才了 5次搞定。大家也可以试一下。
总结
今天我们初步了解了 let, match, loop, break 等基础语句,以及如何添加 crates 依赖到我们的项目中。用这个实战项目作为 Rust 学习的起点,还是非常不错的。下面的章节,我们会进入到更多关于变量,函数,数据类型的主题。
感谢阅读!欢迎评论区交流!
转载自:https://juejin.cn/post/7133988032067633189