第一章 猜数游戏
通过编写这个游戏进一步了解rust的语言特性。
让我们一起动手完成一个项目,来快速上手 Rust!本章将介绍 Rust 中一些常用概念,并通过真实的程序来展示如何运用它们。你将会学到 let
、match
、方法(method)、关联函数(associated function)、外部 crate 等知识!后续章节会深入探讨这些概念的细节。在这一章,我们将练习基础内容。
我们会实现一个经典的新手编程问题:猜猜看游戏。它是这么工作的:程序将会随机生成一个 1 到 100 之间的随机整数。接着它会请玩家猜一个数并输入,然后提示猜测是大了还是小了。如果猜对了,它会打印祝贺信息并退出。
准备一个新项目
要创建一个新项目,进入第一章中创建的 projects 目录,使用 Cargo 新建一个项目,如下:
$ cargo new guessing_game
$ cd guessing_game
第一个命令,
cargo new
,它获取项目的名称(guessing_game
)作为第一个参数。第二个命令进入到新创建的项目目录。
看看生成的 Cargo.toml 文件:
文件名: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]
文件名:src/main.rs
fn main() {
println!("Hello, world!");
}
现在使用 cargo run
命令,一步完成 “Hello, world!” 程序的编译和运行:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 1.50s
Running `target/debug/guessing_game`
Hello, world!
当你需要在项目中快速迭代时,run
命令就能派上用场,正如我们在这个游戏项目中做的,在下一次迭代之前快速测试每一次迭代。
重新打开 src/main.rs 文件。我们将会在这个文件中编写全部的代码。
处理一次猜测
猜猜看程序的第一部分请求和处理用户输入,并检查输入是否符合预期的格式。首先,允许玩家输入猜测。在 src/main.rs 中输入示例 2-1 中的代码。
文件名:src/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}");
}
示例 2-1:获取用户猜测并打印的代码
这些代码包含很多信息,我们一行一行地过一遍。为了获取用户输入并打印结果作为输出,我们需要将 io
输入/输出库引入当前作用域。io
库来自于标准库,也被称为 std
:
use std::io;
默认情况下,Rust 设定了若干个会自动导入到每个程序作用域中的标准库内容,这组内容被称为 预导入(preclude) 内容。你可以在标准库文档中查看预导入的所有内容。
如果你需要的类型不在预导入内容中,就必须使用 use
语句显式地将其引入作用域。std::io
库提供很多有用的功能,包括接收用户输入的功能。
main
函数是程序的入口点:
fn main() {
fn
语法声明了一个新函数,小括号 ()
表明没有参数,大括号 {
作为函数体的开始。
println!
是一个在屏幕上打印字符串的宏:
println!("Guess the number!");
println!("Please input your guess.");
这些代码仅仅打印提示,介绍游戏的内容然后请求用户输入。
使用变量储存值
接下来,创建一个 变量(variable)来储存用户输入,像这样:
let mut guess = String::new();
现在程序开始变得有意思了!这一小行代码发生了很多事。我们使用 let
语句来创建变量。这里是另外一个例子:
let apples = 5;
这行代码新建了一个叫做 apples
的变量并把它绑定到值 5
上。在 Rust 中,变量默认是不可变的,这意味着一旦我们给变量赋值,这个值就不再可以修改了。我们将会在变量与可变性部分详细讨论这个概念。下面的例子展示了如何在变量名前使用 mut
来使一个变量可变:
let apples = 5; // 不可变
let mut bananas = 5; // 可变
注意://
语法开始一个注释,持续到行尾。Rust 忽略注释中的所有内容。
回到猜猜看程序中。现在我们知道了 let mut guess
会引入一个叫做 guess
的可变变量。等号(=
)告诉 Rust 我们现在想将某个值绑定在变量上。等号的右边是 guess
所绑定的值,它是 String::new
的结果,这个函数会返回一个 String
的新实例。String
是一个标准库提供的字符串类型,它是 UTF-8 编码的可增长文本块。
::new
那一行的 ::
语法表明 new
是 String
类型的一个 关联函数(associated function)。关联函数是针对类型实现的,在这个例子中是 String
,而不是 String
的某个特定实例。一些语言中把它称为 静态方法(static method)。
new
函数创建了一个新的空字符串,你会发现很多类型上有 new
函数,因为它是创建类型实例的惯用函数名。
总的来说,let mut guess = String::new();
这一行创建了一个可变变量,当前它绑定到一个新的 String
空实例上。
接收用户输入
回忆一下,我们在程序的第一行使用 use std::io;
从标准库中引入了输入/输出功能。现在调用 io
库中的函数 stdin
:
io::stdin()
.read_line(&mut guess)
如果程序的开头没有使用 use std::io;
引入 io
库,我们仍可以通过把函数调用写成 std::io::stdin
来使用函数。stdin
函数返回一个 std::io::Stdin
的实例,这代表终端标准输入句柄的类型。
代码的下一部分,.read_line(&mut guess)
,调用 read_line
方法从标准输入句柄获取用户输入。我们还将 &mut guess
作为参数传递给 read_line()
函数,让其将用户输入储存到这个字符串中。read_line
的工作是,无论用户在标准输入中键入什么内容,都将其追加(不会覆盖其原有内容)到一个字符串中,因此它需要字符串作为参数。这个字符串参数应该是可变的,以便 read_line
将用户输入附加上去。
&
表示这个参数是一个 引用(reference),它允许多处代码访问同一处数据,而无需在内存中多次拷贝。引用是一个复杂的特性,Rust 的一个主要优势就是安全而简单的操纵引用。完成当前程序并不需要了解如此多细节。现在,我们只需知道它像变量一样,默认是不可变的。因此,需要写成 &mut guess
来使其可变,而不是 &guess
。(第四章会更全面的解释引用。)
使用 Result
类型来处理潜在的错误
我们还没有完全分析完这行代码。虽然我们已经讲到了第三行代码,但要注意:它仍是逻辑行(虽然换行了但仍是语句)的一部分。后一部分是这个方法(method):
.expect("Failed to read line");
我们也可以将代码这样写:
io::stdin().read_line(&mut guess).expect("Failed to read line");
不过,过长的代码行难以阅读,所以最好拆开来写。通常来说,当使用 .method_name()
语法调用方法时引入换行符和空格将长的代码行拆开是明智的。现在来看看这行代码干了什么。
之前提到了 read_line
会将用户输入附加到传递给它的字符串中,不过它也会返回一个类型为 Result
的值。 Result
是一种枚举类型,通常也写作 enum。枚举类型变量的值可以是多种可能状态中的一个。我们把每种可能的状态称为一种 枚举成员(variant)。
第六章将介绍枚举的更多细节。这里的 Result
类型将用来编码错误处理的信息。
Result
的成员是 Ok
和 Err
,Ok
成员表示操作成功,内部包含成功时产生的值。Err
成员则意味着操作失败,并且包含失败的前因后果。
这些 Result
类型的作用是编码错误处理信息。Result
类型的值,像其他类型一样,拥有定义于其上的方法。Result
的实例拥有 expect
方法。如果 io::Result
实例的值是 Err
,expect
会导致程序崩溃,并显示当做参数传递给 expect
的信息。如果 read_line
方法返回 Err
,则可能是来源于底层操作系统错误的结果。如果 Result
实例的值是 Ok
,expect
会获取 Ok
中的值并原样返回。在本例中,这个值是用户输入到标准输入中的字节数。
如果不调用 expect
,程序也能编译,不过会出现一个警告:
$ cargo build
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
warning: unused `Result` that must be used
--> src/main.rs:10:5
|
10 | io::stdin().read_line(&mut guess);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: `#[warn(unused_must_use)]` on by default
= note: this `Result` may be an `Err` variant, which should be handled
warning: `guessing_game` (bin "guessing_game") generated 1 warning
Finished dev [unoptimized + debuginfo] target(s) in 0.59s
Rust 警告我们没有使用 read_line
的返回值 Result
,说明有一个可能的错误没有处理。
消除警告的正确做法是实际去编写错误处理代码,不过由于我们就是希望程序在出现问题时立即崩溃,所以直接使用 expect
。第九章 会学习如何从错误中恢复。
使用 println!
占位符打印值
除了位于结尾的右花括号,目前为止就只有这一行代码值得讨论一下了,就是这一行:
println!("You guessed: {guess}");
这行代码现在打印了存储用户输入的字符串。里面的 {}
是预留在特定位置的占位符:把 {}
想象成小蟹钳,可以夹住合适的值。当打印变量的值时,变量名可以写进大括号中。当打印表达式的执行结果时,格式化字符串(format string)中大括号中留空,格式化字符串后跟逗号分隔的需要打印的表达式列表,其顺序与每一个空大括号占位符的顺序一致。在一个 println!
调用中打印变量和表达式的值看起来像这样:
let x = 5;
let y = 10;
println!("x = {x} and y + 2 = {}", y + 2);
这行代码会打印出 x = 5 and y + 2 = 12
。