Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

编写猜数字游戏

Programming a Guessing Game

让我们通过一起完成一个实战项目来深入了解 Rust!本章将通过展示如何在真实程序中使用一些常见的 Rust 概念来向你介绍它们。你将学习到 letmatch、方法、关联函数、外部 crate 等等!在接下来的章节中,我们将更详细地探讨这些想法。在本章中,你只需练习基础知识。

Let’s jump into Rust by working through a hands-on project together! This chapter introduces you to a few common Rust concepts by showing you how to use them in a real program. You’ll learn about let, match, methods, associated functions, external crates, and more! In the following chapters, we’ll explore these ideas in more detail. In this chapter, you’ll just practice the fundamentals.

我们将实现一个经典的编程入门问题:猜数字游戏。它的工作原理如下:程序将生成一个 1 到 100 之间的随机整数。然后它会提示玩家输入一个猜测。在输入猜测后,程序将指示该猜测是太低还是太高。如果猜测正确,游戏将打印一条祝贺消息并退出。

We’ll implement a classic beginner programming problem: a guessing game. Here’s how it works: The program will generate a random integer between 1 and 100. It will then prompt the player to enter a guess. After a guess is entered, the program will indicate whether the guess is too low or too high. If the guess is correct, the game will print a congratulatory message and exit.

设置新项目

Setting Up a New Project

要设置一个新项目,请转到你在第 1 章中创建的 projects 目录,并使用 Cargo 创建一个新项目,如下所示:

To set up a new project, go to the projects directory that you created in Chapter 1 and make a new project using Cargo, like so:

$ cargo new guessing_game
$ cd guessing_game

第一条命令 cargo new 将项目名称(guessing_game)作为第一个参数。第二条命令切换到新项目的目录。

The first command, cargo new, takes the name of the project (guessing_game) as the first argument. The second command changes to the new project’s directory.

查看生成的 Cargo.toml 文件:

Look at the generated Cargo.toml file:

文件名:Cargo.toml Filename: Cargo.toml

[package]
name = "guessing_game"
version = "0.1.0"
edition = "2024"

[dependencies]

正如你在第 1 章中所看到的,cargo new 为你生成了一个 “Hello, world!” 程序。查看 src/main.rs 文件:

As you saw in Chapter 1, cargo new generates a “Hello, world!” program for you. Check out the src/main.rs file:

文件名:src/main.rs Filename: src/main.rs

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

现在让我们使用 cargo run 命令在同一个步骤中编译并运行这个 “Hello, world!” 程序:

Now let’s compile this “Hello, world!” program and run it in the same step using the cargo run command:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.08s
     Running `target/debug/guessing_game`
Hello, world!

当你需要快速迭代一个项目时,run 命令非常方便,就像我们在这个游戏中要做的那样,在进入下一个迭代之前快速测试每一个迭代。

The run command comes in handy when you need to rapidly iterate on a project, as we’ll do in this game, quickly testing each iteration before moving on to the next one.

重新打开 src/main.rs 文件。你将在这个文件中编写所有的代码。

Reopen the src/main.rs file. You’ll be writing all the code in this file.

处理猜测

Processing a Guess

猜数字程序的第一部分将询问用户输入,处理该输入,并检查输入是否符合预期格式。首先,我们将允许玩家输入一个猜测。在 src/main.rs 中输入示例 2-1 中的代码。

The first part of the guessing game program will ask for user input, process that input, and check that the input is in the expected form. To start, we’ll allow the player to input a guess. Enter the code in Listing 2-1 into 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}");
}

这段代码包含很多信息,所以让我们逐行过一遍。为了获取用户输入并将其作为输出打印,我们需要将 io 输入/输出库引入作用域。io 库来自标准库,即 std

This code contains a lot of information, so let’s go over it line by line. To obtain user input and then print the result as output, we need to bring the io input/output library into scope. The io library comes from the standard library, known as std:

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}");
}

默认情况下,Rust 在标准库中定义了一组项目,并将它们引入每个程序的作用域。这组项目被称为 prelude(预导入),你可以在 标准库文档 中看到其中的所有内容。

By default, Rust has a set of items defined in the standard library that it brings into the scope of every program. This set is called the prelude, and you can see everything in it in the standard library documentation.

如果你想使用的类型不在 prelude 中,你必须使用 use 语句显式地将该类型引入作用域。使用 std::io 库为你提供了许多有用的功能,包括接受用户输入的能力。

If a type you want to use isn’t in the prelude, you have to bring that type into scope explicitly with a use statement. Using the std::io library provides you with a number of useful features, including the ability to accept user input.

正如你在第 1 章中看到的,main 函数是程序的入口点:

As you saw in Chapter 1, the main function is the entry point into the program:

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}");
}

fn 语法声明一个新函数;圆括号 () 表示没有参数;而花括号 { 开始函数体。

The fn syntax declares a new function; the parentheses, (), indicate there are no parameters; and the curly bracket, {, starts the body of the function.

正如你在第 1 章中学到的,println! 是一个将字符串打印到屏幕上的宏:

As you also learned in Chapter 1, println! is a macro that prints a string to the screen:

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}");
}

这段代码打印了一个提示,说明游戏是什么并请求用户输入。

This code is printing a prompt stating what the game is and requesting input from the user.

使用变量存储值

Storing Values with Variables

接下来,我们将创建一个 变量 (variable) 来存储用户输入,如下所示:

Next, we’ll create a variable to store the user input, like this:

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}");
}

现在程序变得有趣了!这一小行里发生了很多事情。我们使用 let 语句来创建变量。这是另一个例子:

Now the program is getting interesting! There’s a lot going on in this little line. We use the let statement to create the variable. Here’s another example:

let apples = 5;

这行创建了一个名为 apples 的新变量,并将其绑定到值 5。在 Rust 中,变量默认是不可变的 (immutable),这意味着一旦我们给变量一个值,该值就不会改变。我们将在第 3 章的 “变量与可变性” 章节中详细讨论这个概念。要使变量可变,我们在变量名前添加 mut

This line creates a new variable named apples and binds it to the value 5. In Rust, variables are immutable by default, meaning once we give the variable a value, the value won’t change. We’ll be discussing this concept in detail in the “Variables and Mutability” section in Chapter 3. To make a variable mutable, we add mut before the variable name:

let apples = 5; // 不可变
let mut bananas = 5; // 可变
let apples = 5; // immutable
let mut bananas = 5; // mutable

注意:// 语法开始一个注释,该注释持续到行尾。Rust 忽略注释中的所有内容。我们将在 第 3 章 中更详细地讨论注释。

Note: The // syntax starts a comment that continues until the end of the line. Rust ignores everything in comments. We’ll discuss comments in more detail in Chapter 3.

回到猜数字程序,你现在知道 let mut guess 将引入一个名为 guess 的可变变量。等号 (=) 告诉 Rust 我们现在想把某些东西绑定到这个变量上。等号右边是 guess 绑定的值,它是调用 String::new 的结果,该函数返回 String 的一个新实例。String 是标准库提供的一种字符串类型,它是可增长的、UTF-8 编码的文本。

Returning to the guessing game program, you now know that let mut guess will introduce a mutable variable named guess. The equal sign (=) tells Rust we want to bind something to the variable now. On the right of the equal sign is the value that guess is bound to, which is the result of calling String::new, a function that returns a new instance of a String. String is a string type provided by the standard library that is a growable, UTF-8 encoded bit of text.

::new 行中的 :: 语法表示 newString 类型的一个关联函数 (associated function)。关联函数 是实现在某个类型上的函数,在本例中是 String。这个 new 函数创建一个新的空字符串。你会在许多类型上找到 new 函数,因为它是创建某种新值的函数的常用名称。

The :: syntax in the ::new line indicates that new is an associated function of the String type. An associated function is a function that’s implemented on a type, in this case String. This new function creates a new, empty string. You’ll find a new function on many types because it’s a common name for a function that makes a new value of some kind.

总而言之,let mut guess = String::new(); 这行创建了一个可变变量,该变量目前绑定到 String 的一个新的空实例。呼!

In full, the let mut guess = String::new(); line has created a mutable variable that is currently bound to a new, empty instance of a String. Whew!

接收用户输入

Receiving User Input

回想一下,我们在程序的第一行使用 use std::io; 包含了标准库中的输入/输出功能。现在我们将调用 io 模块中的 stdin 函数,这将允许我们处理用户输入:

Recall that we included the input/output functionality from the standard library with use std::io; on the first line of the program. Now we’ll call the stdin function from the io module, which will allow us to handle user input:

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}");
}

如果我们没有在程序开头用 use std::io; 导入 io 模块,我们仍然可以通过将此函数调用写成 std::io::stdin 来使用该函数。stdin 函数返回 std::io::Stdin 的一个实例,这是一个代表终端标准输入句柄 (handle) 的类型。

If we hadn’t imported the io module with use std::io; at the beginning of the program, we could still use the function by writing this function call as std::io::stdin. The stdin function returns an instance of std::io::Stdin, which is a type that represents a handle to the standard input for your terminal.

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

Next, the line .read_line(&mut guess) calls the read_line method on the standard input handle to get input from the user. We’re also passing &mut guess as the argument to read_line to tell it what string to store the user input in. The full job of read_line is to take whatever the user types into standard input and append that into a string (without overwriting its contents), so we therefore pass that string as an argument. The string argument needs to be mutable so that the method can change the string’s content.

& 表示该参数是一个 引用 (reference),它为你提供了一种方法,让代码的多个部分访问同一份数据,而无需在内存中多次复制该数据。引用是一个复杂的功能,而 Rust 的主要优势之一就是使用引用的安全性和简便性。你不需要了解很多细节就能完成这个程序。目前,你只需要知道,与变量一样,引用默认也是不可变的。因此,你需要写成 &mut guess 而不是 &guess 来使其可变。(第 4 章将更全面地解释引用。)

The & indicates that this argument is a reference, which gives you a way to let multiple parts of your code access one piece of data without needing to copy that data into memory multiple times. References are a complex feature, and one of Rust’s major advantages is how safe and easy it is to use references. You don’t need to know a lot of those details to finish this program. For now, all you need to know is that, like variables, references are immutable by default. Hence, you need to write &mut guess rather than &guess to make it mutable. (Chapter 4 will explain references more thoroughly.)

使用 Result 处理潜在错误

Handling Potential Failure with Result

我们仍在研究这行代码。我们现在讨论的是第三行文本,但请注意,它仍然是单个逻辑代码行的一部分。下一部分是这个方法:

We’re still working on this line of code. We’re now discussing a third line of text, but note that it’s still part of a single logical line of code. The next part is this method:

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}");
}

我们本来可以将这段代码写成:

We could have written this code as:

io::stdin().read_line(&mut guess).expect("Failed to read line");

然而,过长的一行难以阅读,所以最好将其拆分。当你使用 .method_name() 语法调用方法时,引入换行符和其他空白来帮助拆分长行通常是明智的。现在让我们讨论这行代码的作用。

However, one long line is difficult to read, so it’s best to divide it. It’s often wise to introduce a newline and other whitespace to help break up long lines when you call a method with the .method_name() syntax. Now let’s discuss what this line does.

如前所述,read_line 将用户输入的任何内容放入我们传递给它的字符串中,但它也会返回一个 Result 值。Result 是一个 枚举 (enumeration),通常称为 enum,这是一种可以处于多种可能状态之一的类型。我们称每个可能的状态为一个 变体 (variant)。

As mentioned earlier, read_line puts whatever the user enters into the string we pass to it, but it also returns a Result value. Result is an enumeration, often called an enum, which is a type that can be in one of multiple possible states. We call each possible state a variant.

第 6 章 将更详细地讨论枚举。这些 Result 类型的目的是编码错误处理信息。

Chapter 6 will cover enums in more detail. The purpose of these Result types is to encode error-handling information.

Result 的变体是 OkErrOk 变体表示操作成功,它包含成功生成的值。Err 变体表示操作失败,它包含有关操作如何失败或为何失败的信息。

Result’s variants are Ok and Err. The Ok variant indicates the operation was successful, and it contains the successfully generated value. The Err variant means the operation failed, and it contains information about how or why the operation failed.

与任何类型的值一样,Result 类型的值也定义了方法。Result 的实例有一个你可以调用的 expect 方法。如果这个 Result 实例是一个 Err 值,expect 将导致程序崩溃,并显示你作为参数传递给 expect 的消息。如果 read_line 方法返回 Err,那很可能是底层操作系统发生错误的结果。如果这个 Result 实例是一个 Ok 值,expect 将获取 Ok 持有的返回值,并仅将该值返回给你,以便你可以使用它。在这种情况下,该值是用户输入中的字节数。

Values of the Result type, like values of any type, have methods defined on them. An instance of Result has an expect method that you can call. If this instance of Result is an Err value, expect will cause the program to crash and display the message that you passed as an argument to expect. If the read_line method returns an Err, it would likely be the result of an error coming from the underlying operating system. If this instance of Result is an Ok value, expect will take the return value that Ok is holding and return just that value to you so that you can use it. In this case, that value is the number of bytes in the user’s input.

如果你不调用 expect,程序会编译,但你会得到一个警告:

If you don’t call expect, the program will compile, but you’ll get a warning:

$ 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: this `Result` may be an `Err` variant, which should be handled
   = note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
   |
10 |     let _ = io::stdin().read_line(&mut guess);
   |     +++++++

warning: `guessing_game` (bin "guessing_game") generated 1 warning
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.59s

Rust 警告你没有使用 read_line 返回的 Result 值,这表明程序没有处理可能的错误。

Rust warns that you haven’t used the Result value returned from read_line, indicating that the program hasn’t handled a possible error.

消除警告的正确方法是实际编写错误处理代码,但在我们的例子中,我们只想在出现问题时让程序崩溃,所以我们可以使用 expect。你将在 第 9 章 中学习如何从错误中恢复。

The right way to suppress the warning is to actually write error-handling code, but in our case we just want to crash this program when a problem occurs, so we can use expect. You’ll learn about recovering from errors in Chapter 9.

使用 println! 占位符打印值

Printing Values with println! Placeholders

除了结束花括号,到目前为止代码中只有一行需要讨论:

Aside from the closing curly bracket, there’s only one more line to discuss in the code so far:

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}");
}

这行打印现在包含用户输入的字符串。{} 这对花括号是一个占位符:把 {} 想象成固定值的小螃蟹钳。打印变量的值时,变量名可以放在花括号内。打印表达式求值的结果时,在格式字符串中放置空花括号,然后在格式字符串后面跟随一个逗号分隔的表达式列表,按相同顺序打印在每个空花括号占位符中。在一次 println! 调用中打印一个变量和一个表达式的结果看起来像这样:

This line prints the string that now contains the user’s input. The {} set of curly brackets is a placeholder: Think of {} as little crab pincers that hold a value in place. When printing the value of a variable, the variable name can go inside the curly brackets. When printing the result of evaluating an expression, place empty curly brackets in the format string, then follow the format string with a comma-separated list of expressions to print in each empty curly bracket placeholder in the same order. Printing a variable and the result of an expression in one call to println! would look like this:

#![allow(unused)]
fn main() {
let x = 5;
let y = 10;

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

这段代码会打印 x = 5 and y + 2 = 12

This code would print x = 5 and y + 2 = 12.

测试第一部分

Testing the First Part

让我们测试猜数字游戏的第一部分。使用 cargo run 运行它:

Let’s test the first part of the guessing game. Run it using cargo run:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 6.44s
     Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
6
You guessed: 6

至此,游戏的第一部分已经完成:我们正在从键盘获取输入并将其打印出来。

At this point, the first part of the game is done: We’re getting input from the keyboard and then printing it.

生成一个秘密数字

Generating a Secret Number

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

Next, we need to generate a secret number that the user will try to guess. The secret number should be different every time so that the game is fun to play more than once. We’ll use a random number between 1 and 100 so that the game isn’t too difficult. Rust doesn’t yet include random number functionality in its standard library. However, the Rust team does provide a rand crate with said functionality.

使用 Crate 增加功能

Increasing Functionality with a Crate

请记住,crate 是 Rust 源代码文件的集合。我们一直在构建的项目是一个二进制 crate,它是一个可执行文件。rand crate 是一个库 crate,它包含旨在供其他程序使用的代码,不能独立执行。

Remember that a crate is a collection of Rust source code files. The project we’ve been building is a binary crate, which is an executable. The rand crate is a library crate, which contains code that is intended to be used in other programs and can’t be executed on its own.

Cargo 对外部 crate 的协调正是 Cargo 的闪光点所在。在我们编写使用 rand 的代码之前,我们需要修改 Cargo.toml 文件,将 rand crate 包含为依赖项。现在打开该文件,并在 Cargo 为你创建的 [dependencies] 部分标题下方添加以下行。请务必按照此处的版本号准确指定 rand,否则本教程中的代码示例可能无法工作:

Cargo’s coordination of external crates is where Cargo really shines. Before we can write code that uses rand, we need to modify the Cargo.toml file to include the rand crate as a dependency. Open that file now and add the following line to the bottom, beneath the [dependencies] section header that Cargo created for you. Be sure to specify rand exactly as we have here, with this version number, or the code examples in this tutorial may not work:

文件名:Cargo.toml Filename: Cargo.toml

[dependencies]
rand = "0.8.5"

Cargo.toml 文件中,标题后面的所有内容都属于该部分,直到另一个部分开始。在 [dependencies] 中,你告诉 Cargo 你的项目依赖于哪些外部 crate 以及你需要这些 crate 的哪些版本。在这种情况下,我们使用语义版本说明符 0.8.5 指定 rand crate。Cargo 理解 语义化版本(有时称为 SemVer),这是一种编写版本号的标准。说明符 0.8.5 实际上是 ^0.8.5 的简写,这意味着任何至少为 0.8.5 但低于 0.9.0 的版本。

In the Cargo.toml file, everything that follows a header is part of that section that continues until another section starts. In [dependencies], you tell Cargo which external crates your project depends on and which versions of those crates you require. In this case, we specify the rand crate with the semantic version specifier 0.8.5. Cargo understands Semantic Versioning (sometimes called SemVer), which is a standard for writing version numbers. The specifier 0.8.5 is actually shorthand for ^0.8.5, which means any version that is at least 0.8.5 but below 0.9.0.

Cargo 认为这些版本具有与 0.8.5 版本兼容的公共 API,并且此规范确保你将获得最新的补丁版本,且仍然可以与本章中的代码一起编译。任何 0.9.0 或更高版本都不能保证具有与以下示例中使用的相同的 API。

Cargo considers these versions to have public APIs compatible with version 0.8.5, and this specification ensures that you’ll get the latest patch release that will still compile with the code in this chapter. Any version 0.9.0 or greater is not guaranteed to have the same API as what the following examples use.

现在,在不更改任何代码的情况下,让我们构建项目,如示例 2-2 所示。

Now, without changing any of the code, let’s build the project, as shown in Listing 2-2.

$ cargo build
  Updating crates.io index
   Locking 15 packages to latest Rust 1.85.0 compatible versions
    Adding rand v0.8.5 (available: v0.9.0)
 Compiling proc-macro2 v1.0.93
 Compiling unicode-ident v1.0.17
 Compiling libc v0.2.170
 Compiling cfg-if v1.0.0
 Compiling byteorder v1.5.0
 Compiling getrandom v0.2.15
 Compiling rand_core v0.6.4
 Compiling quote v1.0.38
 Compiling syn v2.0.98
 Compiling zerocopy-derive v0.7.35
 Compiling zerocopy v0.7.35
 Compiling ppv-lite86 v0.2.20
 Compiling rand_chacha v0.3.1
 Compiling rand v0.8.5
 Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
  Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.48s

你可能会看到不同的版本号(但由于 SemVer,它们都将与代码兼容!)和不同的行(取决于操作系统),并且这些行的顺序可能会不同。

You may see different version numbers (but they will all be compatible with the code, thanks to SemVer!) and different lines (depending on the operating system), and the lines may be in a different order.

当我们包含外部依赖项时,Cargo 会从 注册表 (registry) 中提取该依赖项所需的所有内容的最新版本,注册表是来自 Crates.io 的数据副本。Crates.io 是 Rust 生态系统中的人们发布他们的开源 Rust 项目供他人使用的地方。

When we include an external dependency, Cargo fetches the latest versions of everything that dependency needs from the registry, which is a copy of data from Crates.io. Crates.io is where people in the Rust ecosystem post their open source Rust projects for others to use.

更新注册表后,Cargo 会检查 [dependencies] 部分并下载任何列出的但尚未下载的 crate。在这种情况下,虽然我们只将 rand 列为依赖项,但 Cargo 也会抓取 rand 运行所需的其他 crate。下载完 crate 后,Rust 会编译它们,然后在依赖项可用的情况下编译项目。

After updating the registry, Cargo checks the [dependencies] section and downloads any crates listed that aren’t already downloaded. In this case, although we only listed rand as a dependency, Cargo also grabbed other crates that rand depends on to work. After downloading the crates, Rust compiles them and then compiles the project with the dependencies available.

如果你立即再次运行 cargo build 而不做任何更改,除了 Finished 行之外,你不会得到任何输出。Cargo 知道它已经下载并编译了依赖项,并且你没有在 Cargo.toml 文件中更改关于它们的任何内容。Cargo 还知道你没有更改任何代码,因此它也不会重新编译代码。无事可做,它就直接退出了。

If you immediately run cargo build again without making any changes, you won’t get any output aside from the Finished line. Cargo knows it has already downloaded and compiled the dependencies, and you haven’t changed anything about them in your Cargo.toml file. Cargo also knows that you haven’t changed anything about your code, so it doesn’t recompile that either. With nothing to do, it simply exits.

如果你打开 src/main.rs 文件,做一个细微的修改,然后保存并再次构建,你将只看到两行输出:

If you open the src/main.rs file, make a trivial change, and then save it and build again, you’ll only see two lines of output:

$ cargo build
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.13s

这些行显示 Cargo 只根据你对 src/main.rs 文件的微小改动来更新构建。你的依赖项没有改变,所以 Cargo 知道它可以重用已经为它们下载并编译好的内容。

These lines show that Cargo only updates the build with your tiny change to the src/main.rs file. Your dependencies haven’t changed, so Cargo knows it can reuse what it has already downloaded and compiled for those.

确保可重现的构建

Ensuring Reproducible Builds

Cargo 拥有一种机制,可以确保你或任何其他人在构建代码时,每次都能重新构建出相同的产物:除非你另行指定,否则 Cargo 将仅使用你指定的依赖项版本。例如,假设下周 rand crate 的 0.8.6 版本发布了,该版本包含一个重要的错误修复,但也包含一个会导致你的代码崩溃的回退 (regression)。为了处理这个问题,Rust 在你第一次运行 cargo build 时创建了 Cargo.lock 文件,所以我们现在在 guessing_game 目录中有了这个文件。

Cargo has a mechanism that ensures that you can rebuild the same artifact every time you or anyone else builds your code: Cargo will use only the versions of the dependencies you specified until you indicate otherwise. For example, say that next week version 0.8.6 of the rand crate comes out, and that version contains an important bug fix, but it also contains a regression that will break your code. To handle this, Rust creates the Cargo.lock file the first time you run cargo build, so we now have this in the guessing_game directory.

当你第一次构建项目时,Cargo 会找出符合标准的所有依赖项版本,然后将它们写入 Cargo.lock 文件。将来构建项目时,Cargo 会看到 Cargo.lock 文件已存在,并使用其中指定的版本,而不是再次进行找出版本的所有工作。这让你自动拥有了可重现的构建。换句话说,由于有了 Cargo.lock 文件,你的项目将保持在 0.8.5,直到你显式升级。由于 Cargo.lock 文件对于可重现的构建很重要,因此它通常会与项目中的其余代码一起检入源控制系统。

When you build a project for the first time, Cargo figures out all the versions of the dependencies that fit the criteria and then writes them to the Cargo.lock file. When you build your project in the future, Cargo will see that the Cargo.lock file exists and will use the versions specified there rather than doing all the work of figuring out versions again. This lets you have a reproducible build automatically. In other words, your project will remain at 0.8.5 until you explicitly upgrade, thanks to the Cargo.lock file. Because the Cargo.lock file is important for reproducible builds, it’s often checked into source control with the rest of the code in your project.

更新 Crate 以获取新版本

Updating a Crate to Get a New Version

当你 确实 想要更新 crate 时,Cargo 提供了 update 命令,它将忽略 Cargo.lock 文件,并找出符合你 Cargo.toml 中规范的所有最新版本。然后 Cargo 会将这些版本写入 Cargo.lock 文件。否则,默认情况下,Cargo 只会寻找大于 0.8.5 且小于 0.9.0 的版本。如果 rand crate 发布了两个新版本 0.8.6 和 0.999.0,运行 cargo update 时你会看到以下内容:

When you do want to update a crate, Cargo provides the command update, which will ignore the Cargo.lock file and figure out all the latest versions that fit your specifications in Cargo.toml. Cargo will then write those versions to the Cargo.lock file. Otherwise, by default, Cargo will only look for versions greater than 0.8.5 and less than 0.9.0. If the rand crate has released the two new versions 0.8.6 and 0.999.0, you would see the following if you ran cargo update:

$ cargo update
    Updating crates.io index
     Locking 1 package to latest Rust 1.85.0 compatible version
    Updating rand v0.8.5 -> v0.8.6 (available: v0.999.0)

Cargo 忽略了 0.999.0 的发布。此时,你还会注意到 Cargo.lock 文件发生了变化,注明你现在使用的 rand crate 版本是 0.8.6。要使用 rand 版本 0.999.0 或 0.999.x 系列中的任何版本,你必须像这样更新 Cargo.toml 文件(实际上不要做这个修改,因为接下来的示例假设你使用的是 rand 0.8):

Cargo ignores the 0.999.0 release. At this point, you would also notice a change in your Cargo.lock file noting that the version of the rand crate you are now using is 0.8.6. To use rand version 0.999.0 or any version in the 0.999.x series, you’d have to update the Cargo.toml file to look like this instead (don’t actually make this change because the following examples assume you’re using rand 0.8):

[dependencies]
rand = "0.999.0"

下次运行 cargo build 时,Cargo 将更新可用 crate 的注册表,并根据你指定的新版本重新评估你的 rand 要求。

The next time you run cargo build, Cargo will update the registry of crates available and reevaluate your rand requirements according to the new version you have specified.

关于 Cargo 及其 生态系统 还有很多要说的,我们将在第 14 章中讨论,但现在,这就是你需要知道的全部。Cargo 使得重用库变得非常容易,因此 Rust 开发者能够编写由许多包组装而成的较小项目。

There’s a lot more to say about Cargo and its ecosystem, which we’ll discuss in Chapter 14, but for now, that’s all you need to know. Cargo makes it very easy to reuse libraries, so Rustaceans are able to write smaller projects that are assembled from a number of packages.

生成随机数

Generating a Random Number

让我们开始使用 rand 来生成一个要猜的数字。下一步是更新 src/main.rs,如示例 2-3 所示。

Let’s start using rand to generate a number to guess. The next step is to update src/main.rs, as shown in Listing 2-3.

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 定义了随机数生成器实现的方法,为了使用这些方法,该 trait 必须在作用域内。第 10 章将详细介绍 trait。

First, we add the line use rand::Rng;. The Rng trait defines methods that random number generators implement, and this trait must be in scope for us to use those methods. Chapter 10 will cover traits in detail.

接下来,我们在中间添加两行。在第一行中,我们调用 rand::thread_rng 函数,它为我们提供了我们要使用的特定随机数生成器:一个位于当前执行线程本地并由操作系统设定种子的生成器。然后,我们在随机数生成器上调用 gen_range 方法。该方法由我们使用 use rand::Rng; 语句引入作用域的 Rng trait 定义。gen_range 方法接受一个范围表达式作为参数,并在该范围内生成一个随机数。我们在这里使用的这种范围表达式采用 start..=end 的形式,并且包含下限和上限,所以我们需要指定 1..=100 来请求 1 到 100 之间的数字。

Next, we’re adding two lines in the middle. In the first line, we call the rand::thread_rng function that gives us the particular random number generator we’re going to use: one that is local to the current thread of execution and is seeded by the operating system. Then, we call the gen_range method on the random number generator. This method is defined by the Rng trait that we brought into scope with the use rand::Rng; statement. The gen_range method takes a range expression as an argument and generates a random number in the range. The kind of range expression we’re using here takes the form start..=end and is inclusive on the lower and upper bounds, so we need to specify 1..=100 to request a number between 1 and 100.

注意:你不会直接知道从一个 crate 中使用哪些 trait 以及调用哪些方法和函数,所以每个 crate 都有带有使用说明的文档。Cargo 的另一个巧妙之处在于,运行 cargo doc --open 命令将在本地构建所有依赖项提供的文档,并在浏览器中打开。例如,如果你对 rand crate 中的其他功能感兴趣,请运行 cargo doc --open 并点击左侧边栏中的 rand

Note: You won’t just know which traits to use and which methods and functions to call from a crate, so each crate has documentation with instructions for using it. Another neat feature of Cargo is that running the cargo doc --open command will build documentation provided by all your dependencies locally and open it in your browser. If you’re interested in other functionality in the rand crate, for example, run cargo doc --open and click rand in the sidebar on the left.

第二个新行打印秘密数字。这在开发程序时对测试很有用,但我们会在最终版本中将其删除。如果程序一开始就打印出答案,那就没什么游戏性了!

The second new line prints the secret number. This is useful while we’re developing the program to be able to test it, but we’ll delete it from the final version. It’s not much of a game if the program prints the answer as soon as it starts!

试着多运行几次程序:

Try running the program a few times:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.02s
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 7
Please input your guess.
4
You guessed: 4

$ cargo run
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.02s
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 83
Please input your guess.
5
You guessed: 5

你应该会得到不同的随机数,且它们都应该是 1 到 100 之间的数字。做得好!

You should get different random numbers, and they should all be numbers between 1 and 100. Great job!

比较猜测与秘密数字

Comparing the Guess to the Secret Number

现在我们有了用户输入和一个随机数,我们可以比较它们了。这一步如示例 2-4 所示。请注意,正如我们将要解释的那样,这段代码暂时还不能编译。

Now that we have user input and a random number, we can compare them. That step is shown in Listing 2-4. Note that this code won’t compile just yet, as we will explain.

use std::cmp::Ordering;
use std::io;

use rand::Rng;

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

首先,我们添加另一个 use 语句,从标准库中将一个名为 std::cmp::Ordering 的类型引入作用域。Ordering 类型是另一个枚举,其变体为 LessGreaterEqual。这是你比较两个值时可能出现的三种结果。

First, we add another use statement, bringing a type called std::cmp::Ordering into scope from the standard library. The Ordering type is another enum and has the variants Less, Greater, and Equal. These are the three outcomes that are possible when you compare two values.

然后,我们在底部添加五行使用 Ordering 类型的新代码。cmp 方法比较两个值,可以在任何可以比较的对象上调用。它接受一个你想要与之比较的对象的引用:在这里,它将 guesssecret_number 进行比较。然后,它返回一个我们通过 use 语句引入作用域的 Ordering 枚举变体。我们使用 match 表达式来决定下一步做什么,其依据是调用 cmp 比较 guesssecret_number 的值后返回了哪种 Ordering 变体。

Then, we add five new lines at the bottom that use the Ordering type. The cmp method compares two values and can be called on anything that can be compared. It takes a reference to whatever you want to compare with: Here, it’s comparing guess to secret_number. Then, it returns a variant of the Ordering enum we brought into scope with the use statement. We use a match expression to decide what to do next based on which variant of Ordering was returned from the call to cmp with the values in guess and secret_number.

match 表达式由 arms (分支) 组成。一个 arm 由一个与之匹配的 pattern (模式) 以及如果给定的值符合该 arm 的模式则应运行的代码组成。Rust 获取给定的 match 值,并依次查看每个 arm 的模式。模式和 match 结构是 Rust 强大的功能:它们让你表达代码可能遇到的各种情况,并确保你处理了所有这些情况。这些功能将分别在第 6 章和第 19 章中详细讨论。

A match expression is made up of arms. An arm consists of a pattern to match against, and the code that should be run if the value given to match fits that arm’s pattern. Rust takes the value given to match and looks through each arm’s pattern in turn. Patterns and the match construct are powerful Rust features: They let you express a variety of situations your code might encounter, and they make sure you handle them all. These features will be covered in detail in Chapter 6 and Chapter 19, respectively.

让我们用在这里使用的 match 表达式走一个例子。假设用户猜了 50,而这次随机生成的秘密数字是 38。

Let’s walk through an example with the match expression we use here. Say that the user has guessed 50 and the randomly generated secret number this time is 38.

当代码比较 50 和 38 时,cmp 方法将返回 Ordering::Greater,因为 50 大于 38。match 表达式接收 Ordering::Greater 值,并开始检查每个 arm 的模式。它查看第一个 arm 的模式 Ordering::Less,发现值 Ordering::GreaterOrdering::Less 不匹配,因此它忽略该 arm 中的代码并移至下一个 arm。下一个 arm 的模式是 Ordering::Greater,它 确实Ordering::Greater 匹配!该 arm 中的关联代码将执行并打印 Too big! 到屏幕。match 表达式在第一次成功匹配后结束,因此在这种情况下它不会查看最后一个 arm。

When the code compares 50 to 38, the cmp method will return Ordering::Greater because 50 is greater than 38. The match expression gets the Ordering::Greater value and starts checking each arm’s pattern. It looks at the first arm’s pattern, Ordering::Less, and sees that the value Ordering::Greater does not match Ordering::Less, so it ignores the code in that arm and moves to the next arm. The next arm’s pattern is Ordering::Greater, which does match Ordering::Greater! The associated code in that arm will execute and print Too big! to the screen. The match expression ends after the first successful match, so it won’t look at the last arm in this scenario.

然而,示例 2-4 中的代码还不能编译。让我们试试:

However, the code in Listing 2-4 won’t compile yet. Let’s try it:

$ cargo build
   Compiling libc v0.2.86
   Compiling getrandom v0.2.2
   Compiling cfg-if v1.0.0
   Compiling ppv-lite86 v0.2.10
   Compiling rand_core v0.6.2
   Compiling rand_chacha v0.3.0
   Compiling rand v0.8.5
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
error[E0308]: mismatched types
  --> src/main.rs:23:21
   |
23 |     match guess.cmp(&secret_number) {
   |                 --- ^^^^^^^^^^^^^^ expected `&String`, found `&{integer}`
   |                 |
   |                 arguments to this method are incorrect
   |
   = note: expected reference `&String`
              found reference `&{integer}`
note: method defined here
  --> /rustc/1159e78c4747b02ef996e55082b704c09b970588/library/core/src/cmp.rs:979:8

For more information about this error, try `rustc --explain E0308`.
error: could not compile `guessing_game` (bin "guessing_game") due to 1 previous error

错误的核心指出存在 类型不匹配。Rust 拥有强大的静态类型系统。然而,它也具有类型推导功能。当我们编写 let mut guess = String::new() 时,Rust 能够推导出 guess 应该是 String 类型,而无需我们写出类型。另一方面,secret_number 是一个数字类型。Rust 的一些数字类型可以包含 1 到 100 之间的值:i32(32 位数字)、u32(无符号 32 位数字)、i64(64 位数字)等等。除非另有说明,Rust 默认使用 i32,除非你在其他地方添加了会导致 Rust 推导出不同数值类型的类型信息,否则这就是 secret_number 的类型。错误的原因是 Rust 无法比较字符串和数字类型。

The core of the error states that there are mismatched types. Rust has a strong, static type system. However, it also has type inference. When we wrote let mut guess = String::new(), Rust was able to infer that guess should be a String and didn’t make us write the type. The secret_number, on the other hand, is a number type. A few of Rust’s number types can have a value between 1 and 100: i32, a 32-bit number; u32, an unsigned 32-bit number; i64, a 64-bit number; as well as others. Unless otherwise specified, Rust defaults to an i32, which is the type of secret_number unless you add type information elsewhere that would cause Rust to infer a different numerical type. The reason for the error is that Rust cannot compare a string and a number type.

最终,我们想将程序读取为输入的 String 转换为数字类型,以便我们可以将其与秘密数字进行数值比较。我们通过在 main 函数体中添加这一行来做到这一点:

Ultimately, we want to convert the String the program reads as input into a number type so that we can compare it numerically to the secret number. We do so by adding this line to the main function body:

文件名:src/main.rs Filename: src/main.rs

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.");

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

这行代码是:

The line is:

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

我们创建了一个名为 guess 的变量。但等等,程序不是已经有一个名为 guess 的变量了吗?确实如此,但好在 Rust 允许我们用一个新值来 遮蔽 (shadow) guess 之前的值。Shadowing 允许我们重用 guess 变量名,而不是强迫我们创建两个唯一的变量,例如 guess_strguess。我们将在 第 3 章 中更详细地讨论这一点,但目前请记住,当你想将一个值从一种类型转换为另一种类型时,通常会使用此功能。

We create a variable named guess. But wait, doesn’t the program already have a variable named guess? It does, but helpfully Rust allows us to shadow the previous value of guess with a new one. Shadowing lets us reuse the guess variable name rather than forcing us to create two unique variables, such as guess_str and guess, for example. We’ll cover this in more detail in Chapter 3, but for now, know that this feature is often used when you want to convert a value from one type to another type.

我们将此新变量绑定到表达式 guess.trim().parse()。表达式中的 guess 指的是包含字符串输入的原始 guess 变量。String 实例上的 trim 方法将消除开头和结尾的任何空白,在我们将字符串转换为只能包含数值数据的 u32 之前,必须这样做。用户必须按下 enter 才能满足 read_line 并输入他们的猜测,这会在字符串中添加一个换行符。例如,如果用户输入 5 并按下 enterguess 看起来像这样:5\n\n 代表 “换行”。(在 Windows 上,按下 enter 会导致回车和换行,即 \r\n。)trim 方法会消除 \n\r\n,从而只得到 5

We bind this new variable to the expression guess.trim().parse(). The guess in the expression refers to the original guess variable that contained the input as a string. The trim method on a String instance will eliminate any whitespace at the beginning and end, which we must do before we can convert the string to a u32, which can only contain numerical data. The user must press enter to satisfy read_line and input their guess, which adds a newline character to the string. For example, if the user types 5 and presses enter, guess looks like this: 5\n. The \n represents “newline.” (On Windows, pressing enter results in a carriage return and a newline, \r\n.) The trim method eliminates \n or \r\n, resulting in just 5.

字符串上的 parse 方法将字符串转换为另一种类型。在这里,我们使用它将字符串转换为数字。我们需要通过使用 let guess: u32 来告诉 Rust 我们确切需要的数字类型。guess 后面的冒号 (:) 告诉 Rust 我们将注解变量的类型。Rust 有几种内置的数字类型;这里看到的 u32 是一个无符号的 32 位整数。它是小正数的良好默认选择。你将在 第 3 章 中学习其他数字类型。

The parse method on strings converts a string to another type. Here, we use it to convert from a string to a number. We need to tell Rust the exact number type we want by using let guess: u32. The colon (:) after guess tells Rust we’ll annotate the variable’s type. Rust has a few built-in number types; the u32 seen here is an unsigned, 32-bit integer. It’s a good default choice for a small positive number. You’ll learn about other number types in Chapter 3.

此外,此示例程序中的 u32 注解以及与 secret_number 的比较意味着 Rust 也会推导出 secret_number 应该是 u32。所以,现在比较将在两个相同类型的值之间进行!

Additionally, the u32 annotation in this example program and the comparison with secret_number means Rust will infer that secret_number should be a u32 as well. So, now the comparison will be between two values of the same type!

parse 方法仅适用于可以逻辑转换为数字的字符,因此很容易导致错误。例如,如果字符串包含 A👍%,则无法将其转换为数字。由于它可能会失败,因此 parse 方法返回 Result 类型,就像 read_line 方法一样(前面在 “使用 Result 处理潜在错误” 中讨论过)。我们将通过再次使用 expect 方法以同样的方式处理此 Result。如果 parse 返回 Err Result 变体,因为无法从字符串创建数字,则 expect 调用将使游戏崩溃并打印我们提供的信息。如果 parse 可以成功将字符串转换为数字,它将返回 ResultOk 变体,而 expect 将从 Ok 值中返回我们想要的数字。

The parse method will only work on characters that can logically be converted into numbers and so can easily cause errors. If, for example, the string contained A👍%, there would be no way to convert that to a number. Because it might fail, the parse method returns a Result type, much as the read_line method does (discussed earlier in “Handling Potential Failure with Result). We’ll treat this Result the same way by using the expect method again. If parse returns an Err Result variant because it couldn’t create a number from the string, the expect call will crash the game and print the message we give it. If parse can successfully convert the string to a number, it will return the Ok variant of Result, and expect will return the number that we want from the Ok value.

现在运行程序:

Let’s run the program now:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.26s
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 58
Please input your guess.
  76
You guessed: 76
Too big!

不错!即使猜测之前添加了空格,程序仍然算出用户猜的是 76。多次运行程序以验证不同种类输入的各种行为:正确猜中数字、猜一个过高的数字以及猜一个过低的数字。

Nice! Even though spaces were added before the guess, the program still figured out that the user guessed 76. Run the program a few times to verify the different behavior with different kinds of input: Guess the number correctly, guess a number that is too high, and guess a number that is too low.

我们现在已经完成了游戏的大部分工作,但用户只能进行一次猜测。让我们通过添加循环来改变这一点!

We have most of the game working now, but the user can make only one guess. Let’s change that by adding a loop!

使用循环允许多次猜测

Allowing Multiple Guesses with Looping

loop 关键字创建一个无限循环。我们将添加一个循环,让用户有更多机会猜测数字:

The loop keyword creates an infinite loop. We’ll add a loop to give users more chances at guessing the number:

文件名:src/main.rs Filename: src/main.rs

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);

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

如你所见,我们将从猜测输入提示开始的所有内容都移到了循环中。确保将循环内的各行代码再缩进四个空格,并再次运行程序。程序现在将永远要求进行另一次猜测,这实际上引入了一个新问题。看起来用户无法退出了!

As you can see, we’ve moved everything from the guess input prompt onward into a loop. Be sure to indent the lines inside the loop another four spaces each and run the program again. The program will now ask for another guess forever, which actually introduces a new problem. It doesn’t seem like the user can quit!

用户始终可以使用键盘快捷键 ctrl-C 来中断程序。但正如 “比较猜测与秘密数字” 中关于 parse 的讨论提到的,还有另一种方法可以逃离这个贪婪的怪物:如果用户输入非数字答案,程序将崩溃。我们可以利用这一点来允许用户退出,如下所示:

The user could always interrupt the program by using the keyboard shortcut ctrl-C. But there’s another way to escape this insatiable monster, as mentioned in the parse discussion in “Comparing the Guess to the Secret Number”: If the user enters a non-number answer, the program will crash. We can take advantage of that to allow the user to quit, as shown here:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.23s
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 59
Please input your guess.
45
You guessed: 45
Too small!
Please input your guess.
60
You guessed: 60
Too big!
Please input your guess.
59
You guessed: 59
You win!
Please input your guess.
quit

thread 'main' panicked at src/main.rs:28:47:
Please type a number!: ParseIntError { kind: InvalidDigit }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

输入 quit 将退出游戏,但你会注意到,输入任何其他非数字输入也会退出。这至少可以说是不够理想的;我们希望在猜对数字时游戏也能停止。

Typing quit will quit the game, but as you’ll notice, so will entering any other non-number input. This is suboptimal, to say the least; we want the game to also stop when the correct number is guessed.

猜对后退出

Quitting After a Correct Guess

让我们通过添加 break 语句来编写游戏在用户获胜时退出的程序:

Let’s program the game to quit when the user wins by adding a break statement:

文件名:src/main.rs Filename: src/main.rs

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}");

        // --snip--

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}

You win! 之后添加 break 行使程序在用户正确猜中秘密数字时退出循环。由于循环是 main 的最后一部分,退出循环也意味着退出程序。

Adding the break line after You win! makes the program exit the loop when the user guesses the secret number correctly. Exiting the loop also means exiting the program, because the loop is the last part of main.

处理无效输入

Handling Invalid Input

为了进一步完善游戏的表现,与其在用户输入非数字时使程序崩溃,不如让游戏忽略非数字,以便用户可以继续猜测。我们可以通过修改将 guessString 转换为 u32 的那行代码来实现,如示例 2-5 所示。

To further refine the game’s behavior, rather than crashing the program when the user inputs a non-number, let’s make the game ignore a non-number so that the user can continue guessing. We can do that by altering the line where guess is converted from a String to a u32, as shown in Listing 2-5.

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();

        // --snip--

        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}");

        // --snip--

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}

我们将 expect 调用切换为 match 表达式,以便从出错时崩溃转变为处理错误。请记住,parse 返回一个 Result 类型,而 Result 是一个包含 OkErr 变体的枚举。我们在这里使用 match 表达式,就像我们处理 cmp 方法的 Ordering 结果一样。

We switch from an expect call to a match expression to move from crashing on an error to handling the error. Remember that parse returns a Result type and Result is an enum that has the variants Ok and Err. We’re using a match expression here, as we did with the Ordering result of the cmp method.

如果 parse 能够成功地将字符串转换为数字,它将返回一个包含生成数字的 Ok 值。该 Ok 值将匹配第一个 arm 的模式,而 match 表达式将直接返回 parse 产生的并放在 Ok 值内的 num 值。该数字最终会出现在我们正在创建的新 guess 变量中。

If parse is able to successfully turn the string into a number, it will return an Ok value that contains the resultant number. That Ok value will match the first arm’s pattern, and the match expression will just return the num value that parse produced and put inside the Ok value. That number will end up right where we want it in the new guess variable we’re creating.

如果 parse 能将字符串转换为数字,它将返回一个包含有关错误更多信息的 Err 值。Err 值不匹配第一个 match arm 中的 Ok(num) 模式,但它确实匹配第二个 arm 中的 Err(_) 模式。下划线 _ 是一个全匹配 (catch-all) 值;在这个例子中,我们要表达的是我们想匹配所有的 Err 值,不管它们包含什么信息。因此,程序将执行第二个 arm 的代码 continue,它告诉程序进入 loop 的下一次迭代并请求另一个猜测。所以实际上,程序忽略了 parse 可能遇到的所有错误!

If parse is not able to turn the string into a number, it will return an Err value that contains more information about the error. The Err value does not match the Ok(num) pattern in the first match arm, but it does match the Err(_) pattern in the second arm. The underscore, _, is a catch-all value; in this example, we’re saying we want to match all Err values, no matter what information they have inside them. So, the program will execute the second arm’s code, continue, which tells the program to go to the next iteration of the loop and ask for another guess. So, effectively, the program ignores all errors that parse might encounter!

现在程序中的一切都应该按预期工作了。让我们试试:

Now everything in the program should work as expected. Let’s try it:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.13s
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 61
Please input your guess.
10
You guessed: 10
Too small!
Please input your guess.
99
You guessed: 99
Too big!
Please input your guess.
foo
Please input your guess.
61
You guessed: 61
You win!

棒极了!只需最后一个小调整,我们就完成了猜数字游戏。回想一下,程序仍然在打印秘密数字。这对于测试很有效,但它毁了游戏。让我们删除输出秘密数字的 println!。示例 2-6 显示了最终代码。

Awesome! With one tiny final tweak, we will finish the guessing game. Recall that the program is still printing the secret number. That worked well for testing, but it ruins the game. Let’s delete the println! that outputs the secret number. Listing 2-6 shows the final code.

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);

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

至此,你已成功构建了猜数字游戏。恭喜!

At this point, you’ve successfully built the guessing game. Congratulations!

总结

Summary

这个项目是通过实践向你介绍许多新的 Rust 概念的一种方式:letmatch、函数、外部 crate 的使用等等。在接下来的几章中,你将更详细地学习这些概念。第 3 章涵盖了大多数编程语言都有的概念,例如变量、数据类型和函数,并展示了如何在 Rust 中使用它们。第 4 章探讨了所有权 (ownership),这是使 Rust 与其他语言不同的一个特性。第 5 章讨论了结构体 (struct) 和方法语法,第 6 章解释了枚举如何工作。

This project was a hands-on way to introduce you to many new Rust concepts: let, match, functions, the use of external crates, and more. In the next few chapters, you’ll learn about these concepts in more detail. Chapter 3 covers concepts that most programming languages have, such as variables, data types, and functions, and shows how to use them in Rust. Chapter 4 explores ownership, a feature that makes Rust different from other languages. Chapter 5 discusses structs and method syntax, and Chapter 6 explains how enums work.