Разработка игры-угадайки

RustRustIntermediate
Практиковаться сейчас

This tutorial is from open-source community. Access the source code

💡 Этот учебник переведен с английского с помощью ИИ. Чтобы просмотреть оригинал, вы можете перейти на английский оригинал

Введение

Добро пожаловать в Разработку игры в угадайку. Эта лабораторная работа является частью Rust Book. Вы можете практиковать свои навыки Rust в LabEx.

В этой лабораторной работе мы реализуем игру в угадайку на Rust, где программа генерирует случайное число и просит игрока угадать его, давая обратную связь о том, был ли ответ слишком низким или слишком высоким, и поздравляя игрока, если он угадал правильно.

Это Guided Lab, который предоставляет пошаговые инструкции, чтобы помочь вам учиться и практиковаться. Внимательно следуйте инструкциям, чтобы выполнить каждый шаг и получить практический опыт. Исторические данные показывают, что это лабораторная работа уровня средний с процентом завершения 65%. Он получил 100% положительных отзывов от учащихся.

Разработка игры в угадайку

Давайте окунемся в Rust, выполнив практический проект вместе! В этом разделе мы познакомим вас с несколькими общими концепциями Rust, показав, как их использовать в реальной программе. Вы узнаете о let, match, методах, ассоциированных функциях, внешних пакетах и многом другом! В следующих разделах мы рассмотрим эти идеи более подробно. В этом разделе вы только потренируетесь в основах.

Мы реализуем классическую задачу для начинающих программистов: игру в угадайку. Вот как это работает: программа генерирует случайное целое число от 1 до 100. Затем она просит игрока ввести вариант ответа. После ввода ответа программа сообщает, был ли ответ слишком низким или слишком высоким. Если ответ правильный, игра выводит поздравительное сообщение и завершается.

Создание нового проекта

Для создания нового проекта перейдите в директорию project, которую вы создали в главе 1, и создайте новый проект с помощью 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"

## См. дополнительные ключи и их определения по адресу
https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

Как вы видели в главе 1, cargo new генерирует для вас программу "Hello, world!". Посмотрите на файл src/main.rs:

Имя файла: src/main.rs

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

Теперь скомпилируем эту программу "Hello, world!" и запустим ее в одном и том же шаге с помощью команды cargo run:

$ 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. В этом файле вы будете писать весь код.

Обработка ответа

Первая часть программы игры в угадайку будет запрашивать ввод от пользователя, обрабатывать этот ввод и проверять, что ввод имеет ожидаемый формат. Для начала давайте позволим игроку ввести вариант ответа. Введите код из Листинга 2-1 в src/main.rs.

Имя файла: 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 comes from the стандартной библиотеке, известной как std:

use std::io;

По умолчанию в Rust определена группа элементов в стандартной библиотеке, которая включается в область видимости каждого проекта. Эта группа называется предварительным списком, и вы можете увидеть все элементы в ней по адресу https://doc.rust-lang.org/std/prelude/index.html.

Если тип, который вы хотите использовать, не находится в предварительном списке, вы должны явно включить этот тип в область видимости с помощью инструкции use. Использование библиотеки std::io предоставляет вам ряд полезных функций, включая возможность принимать ввод от пользователя.

Как вы видели в главе 1, функция main является точкой входа в программу:

fn main() {

Синтаксис fn объявляет новую функцию; круглые скобки () указывают, что параметров нет; а фигурная скобка { начинает тело функции.

Как вы также узнали в главе 1, println! - это макрос, который выводит строку на экран:

println!("Guess the number!");

println!("Please input your guess.");

Этот код выводит подсказку, которая объясняет, что это за игра, и просит пользователя ввести вариант ответа.

Хранение значений в переменных

Далее мы создадим переменную, чтобы сохранить ввод пользователя, вот так:

let mut guess = String::new();

Теперь программа становится интересной! В этой небольшой строке происходит много вещей. Мы используем инструкцию let для создания переменной. Вот еще один пример:

let apples = 5;

Эта строка создает новую переменную с именем apples и связывает ее со значением 5. В Rust переменные по умолчанию являются неизменяемыми, что означает, что一旦 мы присвоим переменной значение, оно не изменится. Мы подробно обсудим этот концепт в разделе "Переменные и изменяемость". Чтобы сделать переменную изменяемой, мы добавляем mut перед именем переменной:

let apples = 5; // неизменяемая
let mut bananas = 5; // изменяемая

Примечание: Синтаксис // начинает комментарий, который продолжается до конца строки. Rust игнорирует все, что находится в комментариях. Мы обсудим комментарии более подробно в главе 3.

Вернемся к программе игры в угадайку. Теперь вы знаете, что let mut guess создаст изменяемую переменную с именем guess. Знак равно (=) говорит Rust, что мы хотим сейчас связать что-то с переменной. С правой стороны знака равно находится значение, к которому связана guess, которое является результатом вызова String::new, функции, которая возвращает новую инстанцию String. String - это строковый тип, предоставляемый стандартной библиотекой, который представляет собой расширяемый, UTF-8 закодированный кусок текста.

Синтаксис :: в строке ::new указывает, что new - это ассоциированная функция типа String. Ассоциированная функция - это функция, реализованная на типе, в этом случае String. Эта функция new создает новую, пустую строку. Вы найдете функцию new у многих типов, потому что это общее название для функции, которая создает новое значение какого-то типа.

В целом, строка let mut guess = String::new(); создала изменяемую переменную, которая в настоящее время связана с новой, пустой инстанцией String. Фу!

Получение ввода от пользователя

Помните, что мы подключили функциональность ввода/вывода из стандартной библиотеки с помощью use std::io; на первой строке программы. Теперь мы вызовем функцию stdin из модуля io, которая позволит нам обрабатывать ввод пользователя:

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

Если бы мы не импортировали библиотеку io с помощью use std::io; в начале программы, мы все равно могли бы использовать функцию, написав вызов этой функции в виде std::io::stdin. Функция stdin возвращает экземпляр std::io::Stdin, который представляет собой тип, представляющий собой дескриптор стандартного ввода для вашего терминала.

Далее, строка .read_line(&mut guess) вызывает метод read_line на дескрипторе стандартного ввода, чтобы получить ввод от пользователя. Мы также передаем &mut guess в качестве аргумента в read_line, чтобы указать, в какую строку сохранить ввод пользователя. Полная задача read_line - это взять то, что ввел пользователь в стандартный ввод, и добавить его в строку (без перезаписи ее содержимого), поэтому мы передаем эту строку в качестве аргумента. Аргумент-строка должна быть изменяемой, чтобы метод мог изменить содержимое строки.

& указывает, что этот аргумент - это ссылка, которая дает вам способ позволить нескольким частям вашего кода обращаться к одной части данных, не копируя эти данные в память несколько раз. Ссылки - это сложный функционал, и одним из основных преимуществ Rust является то, насколько безопасно и легко использовать ссылки. Вам не нужно знать много деталей, чтобы завершить эту программу. На данный момент все, что вам нужно знать, - это то, что, как и переменные, ссылки по умолчанию являются неизменяемыми. Поэтому вам нужно написать &mut guess, а не &guess, чтобы сделать ее изменяемой. (Глава 4 более подробно объяснит ссылки.)

Обработка возможных ошибок с помощью Result

Мы все еще работаем над этой строкой кода. Теперь мы обсуждаем третью строку текста, но обратите внимание, что это по-прежнему часть одной логической строки кода. Следующая часть - это этот метод:

.expect("Failed to read line");

Мы могли бы написать этот код так:

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

Однако одна длинная строка трудно читать, поэтому лучше разделить ее. Часто имеет смысл вставить перевод строки и другие пробелы, чтобы разбить длинные строки, когда вы вызываете метод с синтаксисом .method_name(). Теперь давайте обсудим, что делает эта строка.

Как упоминалось ранее, read_line помещает то, что вводит пользователь, в строку, которую мы передаем ей, но она также возвращает значение Result. Result - это перечисление, часто называемое enum, которое представляет собой тип, который может находиться в одном из нескольких возможных состояний. Мы называем каждое возможное состояние вариантом.

Глава 6 более подробно рассмотрит enums. Цель этих типов Result - это закодирование информации об обработке ошибок.

Варианты Result - это Ok и Err. Вариант Ok указывает, что операция прошла успешно, и внутри Ok находится успешно сгенерированное значение. Вариант Err означает, что операция завершилась неудачей, и Err содержит информацию о том, как или почему операция завершилась неудачей.

Значения типа Result, как и значения любого типа, имеют определенные на них методы. Экземпляр Result имеет метод expect, который вы можете вызвать. Если этот экземпляр 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 предупреждает, что вы не использовали значение Result, возвращаемое из read_line, что означает, что программа не обрабатывает возможную ошибку.

Правильный способ подавить предупреждение - это на самом деле написать код обработки ошибок, но в нашем случае мы просто хотим аварийно завершить эту программу, когда возникает проблема, поэтому мы можем использовать expect. В главе 9 вы узнаете, как восстанавливаться после ошибок.

Печать значений с помощью плейсхолдеров println!

За исключением закрывающей фигурной скобки, в коде до сих пор осталась только одна строка для обсуждения:

println!("You guessed: {guess}");

Эта строка выводит строку, которая теперь содержит ввод пользователя. Набор фигурных скобок {} - это плейсхолдер: представьте {} в виде маленьких щупалок краба, которые удерживают значение на месте. Когда выводится значение переменной, имя переменной можно поместить внутри фигурных скобок. Когда выводится результат вычисления выражения, в строке форматирования нужно поставить пустые фигурные скобки, а затем за строкой форматирования следует запятая разделенный список выражений, которые будут выведены в каждый пустой плейсхолдер фигурных скобок в том же порядке. Вывод переменной и результата выражения в одном вызове println! будет выглядеть так:

let x = 5;
let y = 10;

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

Этот код выведет x = 5 and y = 12.

Тестирование первой части

Попробуем протестировать первую часть игры в угадайку. Запустите ее с помощью cargo run:

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

На этом этапе первая часть игры завершена: мы получаем ввод с клавиатуры и затем выводим его.

Генерация секретного числа

Далее, нам нужно сгенерировать секретное число, которое пользователь будет пытаться угадать. Секретное число должно быть разным каждый раз, чтобы игра была интересной играть несколько раз. Мы будем использовать случайное число от 1 до 100, чтобы игра не была слишком сложной. В стандартной библиотеке Rust еще нет функциональности для генерации случайных чисел. Однако команда Rust предоставляет пакет rand по адресу https://crates.io/crates/rand с указанной функциональностью.

Использование пакета для получения дополнительной функциональности

Помните, что пакет (crate) - это коллекция файлов с исходным кодом на Rust. Проект, который мы一直在构建,является бинарным пакетом, то есть исполняемым файлом. Пакет rand - это библиотечный пакет, который содержит код, предназначенный для использования в других программах и не может быть запущен самостоятельно.

Взаимодействие Cargo с внешними пакетами - это то, где Cargo действительно превосходит. Перед тем, как мы сможем написать код, использующий rand, нам нужно изменить файл Cargo.toml, чтобы включить пакет rand в качестве зависимости. Теперь откройте этот файл и добавьте следующую строку в конец, под заголовком [dependencies], который Cargo создал для вас. Убедитесь, что указать rand именно так, как здесь, с этой версией номера, иначе примеры кода в этом руководстве могут не работать:

Имя файла: Cargo.toml

[dependencies]
rand = "0.8.5"

В файле Cargo.toml все, что следует после заголовка, является частью этого раздела, который продолжается до начала следующего раздела. В разделе [dependencies] вы говорите Cargo, какие внешние пакеты зависит ваш проект и какие версии этих пакетов вы требуете. В этом случае мы указываем пакет rand с семантическим спецификатором версии 0.8.5. Cargo понимает Семантическое Версионирование (иногда называемое SemVer), которое является стандартом для записи номеров версий. Спецификатор 0.8.5 на самом деле сокращение для ^0.8.5, что означает любую версию, которая не менее 0.8.5, но ниже 0.9.0.

Cargo считает, что эти версии имеют совместимые с версией 0.8.5 публичные API, и это спецификация гарантирует, что вы получите последний патч-релиз, который по-прежнему будет компилироваться с кодом в этой главе. Любая версия 0.9.0 или выше не гарантируется, что имеет такой же API, как в следующих примерах.

Теперь, не меняя никакого кода, давайте построим проект, как показано в Листинге 2-2.

$ cargo build
    Updating crates.io index
  Downloaded rand v0.8.5
  Downloaded libc v0.2.127
  Downloaded getrandom v0.2.7
  Downloaded cfg-if v1.0.0
  Downloaded ppv-lite86 v0.2.16
  Downloaded rand_chacha v0.3.1
  Downloaded rand_core v0.6.3
   Compiling rand_core v0.6.3
   Compiling libc v0.2.127
   Compiling getrandom v0.2.7
   Compiling cfg-if v1.0.0
   Compiling ppv-lite86 v0.2.16
   Compiling rand_chacha v0.3.1
   Compiling rand v0.8.5
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 2.53s

Листинг 2-2: Вывод после запуска cargo build после добавления пакета rand в качестве зависимости

Вы можете увидеть разные номера версий (но они все будут совместимы с кодом, благодаря SemVer!) и разные строки (зависит от операционной системы), и строки могут быть в другом порядке.

Когда мы включаем внешнюю зависимость, Cargo получает последние версии всех зависимостей из реестра, который является копией данных с Crates.io по адресу https://crates.io. Crates.io - это место, где люди в Rust-экосистеме публикуют свои открытые исходные проекты на Rust для использования другими.

После обновления реестра Cargo проверяет раздел [dependencies] и загружает любые пакеты, перечисленные там, которые еще не были загружены. В этом случае, хотя мы только указали rand в качестве зависимости, Cargo также загрузил другие пакеты, от которых зависит rand для работы. После загрузки пакетов Rust компилирует их и затем компилирует проект с доступными зависимостями.

Если вы сразу же снова запустите cargo build без внесения изменений, вы не получите никакого вывода, кроме строки Finished. Cargo знает, что уже загрузил и скомпилировал зависимости, и вы ничего не изменили в файле Cargo.toml по отношению к ним. Cargo также знает, что вы ничего не изменили в своем коде, поэтому он не компилирует его снова. Нет ничего для выполнения, поэтому он просто завершает работу.

Если вы откроете файл src/main.rs, внесете тривиальное изменение, сохраните его и снова запустите сборку, вы увидите только две строки вывода:

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

Эти строки показывают, что Cargo обновляет сборку только с вашим малым изменением в файле src/main.rs. Ваши зависимости не изменились, поэтому Cargo знает, что может использовать то, что уже загрузил и скомпилировал для них.

Гарантия воспроизводимых сборок с помощью файла Cargo.lock

Cargo имеет механизм, который гарантирует, что вы можете повторно собрать один и тот же артефакт каждый раз, когда вы или кто-то другой собирает ваш код: Cargo будет использовать только те версии зависимостей, которые вы указали, пока вы не укажешь иначе. Например, предположим, что на следующей неделе выходит версия 0.8.6 пакета rand, и эта версия содержит важное исправление ошибки, но также содержит регрессию, которая сломает ваш код. Чтобы обработать это, Rust создает файл Cargo.lock в первый раз, когда вы запускаете cargo build, поэтому теперь он находится в директории guessing_game.

Когда вы впервые собираете проект, Cargo определяет все версии зависимостей, которые соответствуют критериям, и затем записывает их в файл Cargo.lock. Когда вы будете собирать свой проект в будущем, Cargo увидит, что файл Cargo.lock существует и будет использовать указанные в нем версии, вместо того чтобы снова выполнять все действия по определению версий. Это позволяет вам автоматически получать воспроизводимую сборку. Другими словами, ваш проект останется на версии 0.8.5, пока вы не явно обновите его, благодаря файлу Cargo.lock. Поскольку файл Cargo.lock важен для воспроизводимых сборок, он часто добавляется в систему контроля версий вместе с остальным кодом вашего проекта.

Обновление пакета для получения новой версии

Когда вы хотите обновить пакет, Cargo предоставляет команду update, которая игнорирует файл Cargo.lock и определяет все последние версии, соответствующие вашим спецификациям в Cargo.toml. Затем Cargo запишет эти версии в файл Cargo.lock. В противном случае, по умолчанию, Cargo будет искать только версии, большие 0.8.5 и меньшие 0.9.0. Если пакет rand выпустил две новые версии 0.8.6 и 0.9.0, то при запуске cargo update вы увидите следующее:

$ cargo update
Updating crates.io index
Updating rand v0.8.5 - > v0.8.6

Cargo игнорирует выпуск 0.9.0. В этом случае вы также заметите изменение в файле Cargo.lock, которое будет указывать, что версия пакета rand, которую вы сейчас используете, равна 0.8.6. Чтобы использовать версию rand 0.9.0 или любую версию в серии 0.9._x_, вам нужно обновить файл Cargo.toml так, чтобы он выглядел следующим образом:

[dependencies]
rand = "0.9.0"

В следующий раз, когда вы запустите cargo build, Cargo обновит реестр доступных пакетов и переоценит ваши требования к rand в соответствии с новой версией, которую вы указали.

Многое можно сказать о Cargo и его экосистеме, о чем мы поговорим в главе 14, но на данный момент это все, что вам нужно знать. Cargo делает использование библиотек очень простым, поэтому Rustaceans могут писать более мелкие проекты, собираемые из нескольких пакетов.

Генерация случайного числа

Давайте начнем использовать rand для генерации числа, которое нужно угадать. Следующим шагом будет обновление src/main.rs, как показано в Листинге 2-3.

Имя файла: src/main.rs

use std::io;
1 use rand::Rng;

fn main() {
    println!("Guess the number!");

  2 let secret_number = rand::thread_rng().gen_range(1..=100);

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

Листинг 2-3: Добавление кода для генерации случайного числа

Во - первых, мы добавляем строку use rand::Rng; [1]. Трейт Rng определяет методы, которые реализуют генераторы случайных чисел, и этот трейт должен быть в области видимости, чтобы мы могли использовать эти методы. Глава 10 подробно рассмотрит трейты.

Далее, мы добавляем две строки посередине. В первой строке [2] мы вызываем функцию rand::thread_rng, которая дает нам конкретный генератор случайных чисел, который мы собираемся использовать: генератор, локальный для текущего потока выполнения и инициализированный операционной системой. Затем мы вызываем метод gen_range на генераторе случайных чисел. Этот метод определен трейтом Rng, который мы подключили к области видимости с помощью инструкции use rand::Rng;. Метод gen_range принимает выражение диапазона в качестве аргумента и генерирует случайное число в этом диапазоне. Тип выражения диапазона, который мы используем здесь, имеет форму start..=end и включает нижнюю и верхнюю границы, поэтому мы должны указать 1..=100, чтобы запросить число от 1 до 100.

Примечание: Вы не сразу поймете, какие трейты использовать и какие методы и функции вызывать из пакета, поэтому каждый пакет имеет документацию с инструкциями по использованию. Еще одна удобная особенность Cargo заключается в том, что запуск команды cargo doc --open будет собирать документацию, предоставленную всеми вашими зависимостями локально и открывать ее в вашем браузере. Если вы интересуетесь другой функциональностью в пакете rand, например, запустите cargo doc --open и нажмите на rand в боковой панели слева.

Вторая новая строка [3] выводит секретное число. Это полезно при разработке программы для ее тестирования, но мы удалим ее из финальной версии. Если программа сразу же выводит ответ, когда запускается, это не очень похоже на игру!

Попробуйте запустить программу несколько раз:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 2.53s
     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 [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. Отличная работа!

Сравнение попытки угадать с секретным числом

Теперь, когда у нас есть ввод пользователя и случайное число, мы можем сравнить их. Этот шаг показан в Листинге 2-4. Обратите внимание, что этот код еще не скомпилируется, как мы объясним.

Имя файла: src/main.rs

use rand::Rng;
1 use std::cmp::Ordering;
use std::io;

fn main() {
    --snip--

    println!("You guessed: {guess}");

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

Листинг 2-4: Обработка возможных возвращаемых значений при сравнении двух чисел

Во - первых, мы добавляем еще одну инструкцию use [1], подключая тип std::cmp::Ordering из стандартной библиотеки в область видимости. Тип Ordering - это еще один перечисление (enum) и имеет варианты Less, Greater и Equal. Эти три результата могут быть при сравнении двух значений.

Затем мы добавляем пять новых строк внизу, которые используют тип Ordering. Метод cmp [3] сравнивает два значения и может быть вызван на любом типе, который может быть сравниваемым. Он принимает ссылку на то, с чем вы хотите сравнивать: здесь он сравнивает guess с secret_number. Затем он возвращает вариант перечисления Ordering, которое мы подключили к области видимости с помощью инструкции use. Мы используем выражение match [2], чтобы определить, что делать дальше, в зависимости от того, какой вариант Ordering был возвращен из вызова cmp с значениями в guess и secret_number.

Выражение match состоит из ветвей (arms). Ветвь состоит из шаблона (pattern), который нужно сопоставить, и кода, который должен выполняться, если значение, переданное в match, соответствует шаблону этой ветви. Rust берет значение, переданное в match, и последовательно просматривает каждый шаблон ветви. Шаблоны и конструкция match - это мощные особенности Rust: они позволяют вам выражать различные ситуации, которые может встретить ваш код, и обеспечивают обработку всех этих ситуаций. Эти особенности будут рассмотрены подробнее в главе 6 и главе 18 соответственно.

Рассмотрим пример с выражением match, которое мы используем здесь. Предположим, что пользователь угадал 50, а случайно сгенерированное секретное число на этот раз равно 38.

Когда код сравнивает 50 с 38, метод cmp вернет Ordering::Greater, потому что 50 больше 38. Выражение match получает значение Ordering::Greater и начинает проверять каждый шаблон ветви. Он смотрит на шаблон первой ветви, Ordering::Less, и видит, что значение Ordering::Greater не соответствует Ordering::Less, поэтому он игнорирует код в этой ветви и переходит к следующей ветви. Шаблон следующей ветви - это Ordering::Greater, который соответствует Ordering::Greater! Связанный с этой ветвью код будет выполнен, и на экран будет выведено Too big!. Выражение match заканчивается после первого успешного сопоставления, поэтому в этом сценарии оно не посмотрит на последнюю ветвь.

Однако код в Листинге 2-4 еще не скомпилируется. Попробуем его:

$ cargo build
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
error[E0308]: mismatched types
  --> src/main.rs:22:21
   |
22 |     match guess.cmp(&secret_number) {
   |                     ^^^^^^^^^^^^^^ expected struct `String`, found integer
   |
   = note: expected reference `&String`
              found reference `&{integer}`

Основная причина ошибки заключается в том, что есть несовместимые типы. Rust имеет сильную статическую систему типов. Однако он также имеет вывод типов (type inference). Когда мы написали let mut guess = String::new(), Rust смог вывести, что guess должен быть String, и не заставил нас указывать тип. С другой стороны, secret_number - это числовой тип. Некоторые числовые типы в Rust могут иметь значение от 1 до 100: i32 (32 - битное число), u32 (беззнаковое 32 - битное число), i64 (64 - битное число) и другие. Если не указано иначе, Rust по умолчанию использует i32, который является типом secret_number, если вы не добавите информацию о типе в другом месте, которая заставит Rust вывести другой числовой тип. Причина ошибки в том, что Rust не может сравнить строку и числовой тип.

В конечном итоге, мы хотим преобразовать String, которое программа читает в качестве ввода, в реальный числовой тип, чтобы мы могли сравнить его численно с секретным числом. Мы делаем это, добавив эту строку в тело функции main:

Имя файла: src/main.rs

use std::io;
use rand::Rng;
use std::cmp::Ordering;

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

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

Мы создаем переменную с именем guess. Но подождите, у программы уже есть переменная с именем guess? Да, но к счастью, Rust позволяет нам "затенять" (shadow) предыдущее значение guess новым. Затенение (shadowing) позволяет нам повторно использовать имя переменной guess, вместо того чтобы заставить нас создавать два уникальных имени переменной, например, guess_str и guess. Мы рассмотрим это более подробно в главе 3, но на данный момент просто запомните, что эта особенность часто используется, когда вы хотите преобразовать значение из одного типа в другой тип.

Мы связываем эту новую переменную с выражением guess.trim().parse(). guess в выражении ссылается на исходную переменную guess, которая содержала ввод в виде строки. Метод trim на экземпляре String удалит все пробелы в начале и в конце, что необходимо сделать, чтобы сравнить строку с u32, который может содержать только числовые данные. Пользователь должен нажать Enter, чтобы удовлетворить read_line и ввести свою догадку, что добавляет символ новой строки в строку. Например, если пользователь вводит 5 и нажимает Enter, guess выглядит так: 5\n. \n представляет "новую строку" (На Windows нажатие Enter приводит к возврату каретки и новой строке, \r\n). Метод trim удаляет \n или \r\n, оставляя только 5.

Метод parse на строках преобразует строку в другой тип. Здесь мы используем его, чтобы преобразовать из строки в число. Мы должны сказать Rust точный числовой тип, который мы хотим, используя let guess: u32. Двоеточие (:) после guess говорит Rust, что мы будем аннотировать тип переменной. Rust имеет несколько встроенных числовых типов; u32, который мы видим здесь, - это беззнаковое, 32 - битное целое число. Это хороший выбор по умолчанию для небольшого положительного числа. Вы узнаете о других числовых типах в главе 3.

此外,аннотация u32 в этом примере программы и сравнение с secret_number означает, что Rust также будет выводить, что secret_number должен быть u32. Теперь сравнение будет между двумя значениями одного типа!

Метод parse будет работать только с символами, которые могут логически быть преобразованы в числа, и поэтому может легко вызывать ошибки. Например, если строка содержала A👍%, невозможно было бы преобразовать это в число. Поскольку это может привести к ошибке, метод parse возвращает тип Result, так же, как и метод read_line (обсуждается ранее в разделе "Обработка потенциальной ошибки с Result"). Мы будем обрабатывать это Result так же, используя метод expect снова. Если parse возвращает вариант Err из Result, потому что он не может создать число из строки, вызов expect упадет с игрой и выведет сообщение, которое мы передаем ему. Если parse может успешно преобразовать строку в число, он вернет вариант Ok из Result, и expect вернет число, которое мы хотим из значения Ok.

Теперь запустим программу:

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

Отлично! Даже несмотря на то, что перед догадкой были добавлены пробелы, программа все равно поняла, что пользователь угадал 76. Запустите программу несколько раз, чтобы проверить разное поведение при разных видах ввода: угадать число правильно, угадать число, которое слишком большое, и угадать число, которое слишком маленькое.

Теперь у нас работает большая часть игры, но пользователь может сделать только одну попытку. Изменим это, добавив цикл!

Позволение множества попыток угадывания с использованием цикла

Ключевое слово loop создает бесконечный цикл. Мы добавим цикл, чтобы дать пользователям больше шансов угадать число:

Имя файла: src/main.rs

use std::io;
use rand::Rng;
use std::cmp::Ordering;

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

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

Как вы можете видеть, мы переместили все, начиная от приглашения ввода попытки, в цикл. Убедитесь, что отступить строки внутри цикла на еще четыре пробела каждая и запустить программу снова. Теперь программа будет постоянно запрашивать новую попытку, что на самом деле представляет новую проблему. Казалось бы, пользователь не может выйти!

Пользователь всегда может прервать программу, используя сочетание клавиш ctrl-C. Но есть еще один способ избавиться от этой неутомимой монстры, как упоминалось в обсуждении parse в разделе "Сравнение попытки угадать с секретным числом": если пользователь вводит нечисловой ответ, программа упадет. Мы можем использовать это, чтобы позволить пользователю выйти, как показано здесь:

$ 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`
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 'Please type a number!: ParseIntError
{ kind: InvalidDigit }', src/main.rs:28:47
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Ввод quit вызовет выход из игры, но, как вы заметите, так же будет выходить при вводе любого другого нечислового ввода. Это, по крайней мере, неоптимально; мы хотим, чтобы игра также останавливалась, когда угадано правильное число.

Завершение работы после правильной догадки

Давайте напишем программу для выхода из игры, когда пользователь угадывает число, добавив инструкцию break:

Имя файла: src/main.rs

--snip--

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

Добавление строки break после You win! заставляет программу выйти из цикла, когда пользователь угадывает секретное число правильно. Выход из цикла также означает выход из программы, потому что цикл является последней частью main.

Обработка некорректного ввода

Для дальнейшего усовершенствования поведения игры, вместо того чтобы программа падала при вводе пользователем нечислового значения, давайте сделаем так, чтобы игра игнорировала нечисловой ввод, чтобы пользователь мог продолжать угадывать. Мы можем это сделать, изменив строку, где guess преобразуется из String в u32, как показано в Листинге 2-5.

Имя файла: src/main.rs

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

Листинг 2-5: Игнорирование нечисловой попытки угадать и запрос нового ввода вместо аварийного завершения программы

Мы переходим от вызова expect к выражению match, чтобы перейти от аварийного завершения при ошибке к обработке ошибки. Помните, что parse возвращает тип Result, а Result - это перечисление, которое имеет варианты Ok и Err. Мы используем выражение match здесь, как и с результатом Ordering метода cmp.

Если parse успешно преобразует строку в число, он вернет значение Ok, которое содержит полученное число. Это значение Ok соответствует шаблону первой ветви, и выражение match просто вернет значение num, которое parse сгенерировал и поместил в значение Ok. Это число окажется в том месте, где мы хотим его в новой переменной guess, которую мы создаем.

Если parse не может преобразовать строку в число, он вернет значение Err, которое содержит дополнительную информацию об ошибке. Значение Err не соответствует шаблону Ok(num) в первой ветви match, но оно соответствует шаблону Err(_) во второй ветви. Нижнее подчеркивание, _, - это значение, которое ловит все; в этом примере мы говорим, что хотим соответствовать всем значениям Err, независимо от информации внутри них. Таким образом, программа выполнит код второй ветви, continue, который говорит программе перейти к следующей итерации loop и запросить новую попытку. Таким образом, по существу, программа игнорирует все ошибки, которые может встретить parse!

Теперь все в программе должно работать, как ожидается. Давайте попробуем:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 4.45s
     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 показывает окончательный код.

Имя файла: src/main.rs

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

Листинг 2-6: Полный код игры-угадайки

На этом этапе вы успешно создали игру-угадайку. Поздравляем!

Резюме

Поздравляем! Вы завершили лабораторную работу по написанию игры-угадайки. Вы можете практиковаться в других лабораторных работах в LabEx, чтобы улучшить свои навыки.