Игра "Угадай число"

Предлагаю начать программирование прямо сейчас! Мы создадим программу на Rust. Будем учиться программированию посредством создания Rust-проекта. Эта глава поможет Вам получить практический опыт работы, познакомит с концепциями языка программирования. Вы научитесь использовать ключевые слова, такие как let и match, познакомитесь с методами, ассоциированными функциями, научитесь использовать внешние модули и многое другое. Мы надеемся, что, изучив материалы этой главы, вы освоите фундаментальные знания теории и практики использования возможностей языка Rust.

Мы реализуем простую задачу: угадывание числа. Алгоритм игры следующий: программа генерирует целое число от 0 до 100. Игрок должен угадать это число. После каждого неправильного ответа даётся подсказка - загаданное число меньше или больше введенного игроком. Если число угадано - победитель принимает поздравления. :-) Программа завершает работу.

Настройка нового проекта

Для создания нового проекта, в строке терминала перейдите в папку projects (в ту, которую Вы создали ранее). С помощью уже знакомой Вам утилиты cargo создадим новый проект:

$ cargo new guessing_game --bin
$ cd guessing_game

or

$ cargo new guessing_game --bin && cd guessing_game

Данная команда cargo new принимает аргумент - имя нового проекта guessing_game, а далее флаг --bin, который уточняет какой тип приложения мы хотим создать. В данном случае - это консольное приложение.

Рассмотрим содержание файла Cargo.toml, созданного в папке нового проекта:

Filename: Cargo.toml

[package]
name = "guessing_game"
version = "0.1.0"
authors = ["Your Name <you@example.com>"]

[dependencies]

Содержание файла src/main.rs такое же (), как и файла в проекте hello_world:

Filename: src/main.rs

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

Компилируем и запускаем программу с помощью команды cargo run:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
     Running `target/debug/guessing_game`
Hello, world!

Используйте команду run, когда нужно быстро скомпилировать и запустить программу на выполнение. Что-то подобное будет происходить и той программе, которую мы будем создавать.

Откройте файл src/main.rs для редактирования. Далее мы будем менять содержание этого шаблонного файла исходного кода программы.

Обработка вводимых данных

Программа начинается с опроса пользователя - необходимо ввести число. Далее программа анализирует ввёденную пользователем информацию. Для начала напишем код, позволяющий ввести данные с клавиатуры. Пожалуйста, замените содержание файла исходного кода src/main.rs на следующий текст:

Filename: 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);
}

Listing 2-1: Программа просит ввести строку, а потом печатает её

Этот программный код содержит много новой для Вас информации. Разберём код шаг за шагом. Для того чтобы считать введённые данные с клавиатуры, а потом вывести их на экран, нам нужна библиотека ввода/вывода io. Эта библиотека входит в состав стандартной библиотеки Rust std.

use std::io;

По умолчанию, Rust загружает несколько типов данных в память, чтобы их можно было бы использовать без каких-либо дополнительных описаний в коде (the prelude). Если типы данных, которые вы хотите использовать в программе не входят в состав этих типов данных вам надо описать их использования явным образом. Это можно сделать с помощью выражения use. Библиотека ввода/вывода std::io предоставляет множество полезных функциональных возможностей, в том числе для обработки вводимых данных пользователя.

Функция main - точка начала выполнения программы:

fn main() {

Синтаксис определения функции следующий: fn - ключевое слово начала описания функции, круглые скобки () - контейнер входных параметров функции, фигурная скобка { - обозначение начала тела функции.

println! - это макрос, которые печатает текст и перемещает курсор на новую строку:

println!("Guess the number!");

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

Этот код просто печатает предложение ввести строку для начала игры и далее печатает введённое значение.

Создание переменной для хранения значений

Далее, мы создаём место хранения введенных игроком данных:

let mut guess = String::new();

Сейчас программа начинает становиться интересной. Обратите, пожалуйста, внимание, как много нового в этой стоке! Прежде всего здесь есть выражение let, которое используется для создания переменной. Вот, например:

let foo = bar;

В этой сроке создаётся переменная с именем foo, которая связывается со значением bar. Особенностью языка Rust является то, что переменные по умолчанию неизменяемые. Этот пример показывает, как использовать ключевое слово mut перед именем переменной для того, чтобы сделать переменную изменяемой:


# #![allow(unused_variables)]
#fn main() {
let foo = 5; // immutable
let mut bar = 5; // mutable
#}

Обратите внимание, что символ // - это ключевое слова обозначающее комментарий, который размещается на одной строке. Всё, что размещено в строке комментария - игнорируется компилятором.

Таким образом выражение let mut guess - это объявление изменяемой переменной с именем guess.


# #![allow(unused_variables)]
#fn main() {
let mut guess = String::new();
#}

Обратите внимание, что это выражение состоит из двух частей разделенных знаком =. С левой частью мы разобрались - это объявление переменной. Теперь разберёмся с правой. Там расположен вызов функции new(). Результат вызова этой функции - экземпляр структуры имеющей тип String. Этот тип данных входит в стандартную библиотеку. Создавая экземпляр String, вы создаёте контейнер, который может хранить строковые данные в кодировке UTF-8. Размер хранящихся данных может изменяться динамически.

Также обратите внимание на синтаксическую конструкцию ::. Выражение ::new сообщает нам следующую информацию: функция new - это функция, которая связана с типом String, а не с экземпляром данного типа. Знатоки языка Java сейчас улыбнуться, увидят тут что-то знакомое. Да, да. Вы не ошиблись - это статический метода типа String.

Результатом вызова Функции new является новая, пустая String. С данной new, я думаю, вы ещё неоднократно столкнётесь изучаю код стандартной библиотеки, т.к. это общее имя функции, которая создаёт экземпляр определённого типа.

Подытожим наш анализ выражения let mut guess = String::new();. Данный код создаёт изменяемую переменную guess, которая связывается с новым пустым экземпляром типа String. Всё просто и ясно. Отлично! Теперь перейдем к следующей строке нашей программы. Рассмотрим длинную строку кода:


# #![allow(unused_variables)]
#fn main() {
io::stdin().read_line(&mut guess).expect("Failed to read line");
#}

Тут мы видим вызов методов read_line и expect стандартной библиотеки std::io. Благодаря тому, что мы заблаговременно сообщили о том, что будем использовать методы данной библиотеки, мы сокращаем наш последующий код. Весьма удобно, не правда ли?! Иначе ... а проверим, что будет иначе! Удалим (нет! просто закомментируем строку кода use std::io;, ведь мы уже знаем, как это делается в Rust!) и посмотрим на ошибки компилятора. Потом добавлять префикс std::io там где он необходим (компилятор нам сообщим, где проблема - какая строка кода имеет ошибку). Нашли? Отлично! проверяйте работу кода с помощью cargo run!

Вот вариант рабочего кода:

//use std::io;

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

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

    let mut guess = String::new();
  // ↓↓ мы добавили префикс std:: ↓↓
    std::io::stdin().read_line(&mut guess)
        .expect("Failed to read line");

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

Также этот код можно переписать следующим образом:

//use std::io;

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

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

    let mut guess = String::new();
  // ↓↓ мы добавили префикс std:: ↓↓
    std::
    io::stdin().read_line(&mut guess)
        .expect("Failed to read line");

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

Функция stdin возвращает экземпляр типа std::io::Stdin, который является обработчиком данных, вводимых с клавиатуры терминала.

Следующая часть кода .read_line(&mut guess) вызывает метод экземпляра этого обработчика read_line. Данный метод производит чтение введенных данных. Обратите внимание на синтаксис описания входных данных этого метода: &mut guess!

Метод read_line добавляет к содержанию переменной guess всё, что введено с клавиатуры. Поэтому очень важно, чтобы переменная, к которой добавляются значения была изменяемой.

Префикс переменной & обозначает, что в функцию мы передаём ссылку. Это даёт возможность непосредственного изменения данных, которые находятся в памяти по данному адресу. Rust проявляет свои преимущества как раз в безопасной работе с такими типами данных. В главе № 4 будет рассказано подробнее об этом типе данных. Важной особенностью, о которой мы узнали из работы с данной строкой кода, является то, что переменные данного типа по умолчанию - неизменяемые. Именно поэтому мы должны описать вводимые данные именно так &mutguess. &guess - данного описания входных данных будет недостаточно, чтобы сообщить компилятору о том, что мы передаём методу read_line ссылку на изменяемые данные. Пожалуйста, проверьте это утверждение на практике! Сначала изменим код так, чтобы некоторые его части можно было бы закомментировать, а потом закомментируем строку содержащую ключевое слов mut.

use std::io;

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

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

    let mut guess = String::new();

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

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

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

Далее перейдём к следующему методы этой длинной цепочки вызовов методов:

.expect("Failed to read line");

На практике мы уже научились размещать код на нескольких строках (это может быть необходимо по разным причинам: для удобства чтения длинных цепочек кода, для включения/отключения некоторых звеньев). Поэтому данная запись:

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

для удобства чтения, разделена на две строки

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

Оставим рассуждения о стиле. Вернёмся к сути! Далее мы продолжим изучать нашу (уже такую близкую и понятную строку кода).

Обработка потенциальных ошибок с помощью типа Result

Как мы уже знаем, функция read_line добавляет строковые данные к содержанию переменной. Помимо этого функция возвращает значение io::Result. В стандартной библиотеке существует множество типов имеющих название Result, а также такими именами называются вложенные модули (например, io::Result).

Типа данных Result чаще всего являются перечисленияenums (enums). Это такой тип данных, который имеет фиксированный набор значений. В главе № 6 мы подробнее познакомится с этим типом данных.

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

Основной целью Result-типов является понятное для последующего анализа информации об ошибке. Значениями Result-типов, так как и любых других типов, являются определённые в них методы. Экземпляр типа io::Result имеет метод expect, который вы можете вызвать. Если экземпляр типа io::Result является значение Err, метод expect завершит работы программы и отобразит информацию об ошибке (с дополнительной информацией, которую вы передали функции):

.expect("Failed to read line");

Если же экземпляром типа io::Result является значение Ok, метод expect возвратит результат работы. Пожалуйста, самостоятельно создайте переменную, присвойте ей результат работы функции expect и напечатайте её содержание в терминальной строке! В данном случае должно быть напечатано количество байт, которое было введено с клавиатуры.

  let number_of_bytes = io::stdin().read_line(&mut guess).expect("Failed to read line");
  println!("number of bytes was entered: {}", number_of_bytes);

Если мы сохраним код программы, закомментировав вызов метода expect:

use std::io;

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

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

    let mut guess = String::new();

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

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

то в терминальной строке будет напечатано предупреждение проблемах вашего кода:

$ cargo build
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
src/main.rs:10:5: 10:39 warning: unused result which must be used,
#[warn(unused_must_use)] on by default
src/main.rs:10     io::stdin().read_line(&mut guess);
                   ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Программа не обрабатывает возможные ошибки. Правильным решением соблюсти требования компилятора будет написать обработчик ошибки. Вызов фукции expect - наиболее простое решение. Боле подробно об обработки ошибок мы познакомится в главе № 9.

Вывод данных с помощью println!

Осталась еще одна строка которую нам необходимо обсудить:

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

Эта команда печатает строку, которую пользователь ввел ранее. Пара фигурных скобок {} — это шаблон, который будет заменен аргументами, следующими за строкой описывающей формат вывода. Чтобы запомнить этот синтаксис представьте, что {} — это маленькие клешни краба, удерживающие значение на месте. Вы можете вывести на экран сразу несколько переменных, используя этот формат. Для этого в строку, описывающую формат, необходимо вставить несколько шаблонов. В этом случае на место первой пары фигурных скобок будет подставлен первый аргумент после строки формата, на место второй пары — второй аргумент и так далее. Вывод нескольких значений в одной строке будет выглядеть так:


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

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

Этот код напечатает x = 5 and y = 10.

Проверка работы вашей текущей версии программы "Угадай число"

Пожалуйста, убедитесь, что ваша программа "Угадай число" работает корректно. Надеемся, что Вы хорошо понимаете написанный код.

use std::io;

fn main() {
    println!("\"Угадай число\"");

    println!("Пожалуйста, введите предположение!");

    let mut guess = String::new();

    io::stdin().read_line(&mut guess)
        .expect("Ой! Что-то случилось! К сожалению, Не удалось прочитать строку. :-(");

    println!("Вы ввели следующие данные: {}", guess);
}

Пример компиляции и выполнения кода вашей программы с троке терминала:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
     Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
6
You guessed: 6

Итак, первая часть (ввод и отображения введённых вами данных) игры готова.

Создание числа для отгадывания

Прежде чем отгадать число нам необходимо его загадать. Программно это делается следующим образом. Нам нужно сгенерировать число и сохранить его в какой-либо переменой. Для того, чтобы игра была интересной для каждой новой игры это число должно быть непредсказуемым. Для упрощения задачи, предположим что это число будет больше 0 и меньше или равно 100. Пока стандартная библиотека не оснащена функциональностью для генерации случайных чисел. Но для этих целей мы можем воспользоваться контейнером (библиотекой) созданным разработчиками языка Rust rand crate.

Использование контейнеров для расширения функциональных возможностей приложений

Внимание! Новая и важная информация - контейнеры crate (их ещё называют пакетами) — это набор переносимого программного определённого созданного по определённым правилам и служащий определённой цели. Эти контейнеры бывают разных видов: бинарные и библиотечные. Одна из главных целей и задача проекта Cargo - помочь разработчикам использовать сторонние пакеты. rand — это один из таких библиотечных пакетов. Для доступа к его функциональным возможностям, прежде всего, надо модифицировать содержание секции [dependencies] файла конфигурации Cargo.toml.

Filename: Cargo.toml

[dependencies]

rand = "0.3.14"

Для того чтобы узнать последнюю версию пакета, пожалуйста воспользуйтесь строкой поиска на сайте crates.io

Обрате внимание, что вся информация в файле конфигурации сгруппирована по секциям. Секция [dependencies] необходима для описания необходимого внешнего пакета. В данном примере мы указали уникальный идентификатор и версию пакета. Обратите внимание, что мы указали семантический номер версии 0.3.14. Менеджер Cargo использует semver(SemVer) - методологию написания и анализа номера версии. В данном случае 0.3.14 - это сокращенная вид полного идентификатора ^0.3.14. Этот текст значит следующее: "любая версия, которая имеет открытый API и совместима с версией 0.3.14".

Будем считать, что Вы внесли вышеописанные изменения в файл Cargo.toml и сохранили данный файл. Теперь можно собрать проект заново:

$ cargo build
    Updating registry `https://github.com/rust-lang/crates.io-index`
 Downloading rand v0.3.14
 Downloading libc v0.2.14
   Compiling libc v0.2.14
   Compiling rand v0.3.14
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)

Listing 2-2: Результат работы команды cargo build после того, как мы добавили информации о зависимости нашего проекта от пакета rand

Результат работы на вашем компьютере может отличаться от данного, т.к. проекты находятся в постоянном развитии (последовательность скачивания и компиляции пакетов также может быть иной).

Теперь ваш проект содержит необходимые внешние контейнеры (пакеты), скаченные из регистратора пакетов Crates.io. Crates.io - это сайт, где публикуются результаты работ сообщества программистов Rust. Все они доступны для использования.

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

Если запустить команду cargo build ещё раз, то компиляции не будет - в ней нет необходимости (если же Вы внесли какие-либо изменения в файл конфигурации Cargo.toml — перекомпиляция произойдёт). Также Cargo отслеживает все изменения в вашем исходном коде. Так что если вы сделаете изменения исходного кода - произойдёт перекомпиляция только вашего проекта. Внешние пакеты перекомпилироваться не будут:

$ cargo build
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)

Защита проекта от изменений внешних контейнеров- файл Cargo.lock

Проект Cargo предлагает механизм защиты проекта от негативных воздействий со стороны сторонних проектов. Он предлагает сравнительно простое решение. Это своеобразный реестр внешних проектов, который позволяет точно определить какие именно внешние пакеты участвовали в удачной сборке и предлагает их использование при перекомпиляции, при переносе проекта. Cargo.lock - как раз является хранилищем этих данных.

При повторной сборке проекта, Cargo, основывается на данных файла Cargo.lock. Проект изменит данные о зависимостях лишь тогда, когда будет явно вызвана команда обновления проекта.

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

Если Вам необходимо обновить внешние связи вашего проекта, Cargo предлагает для этой цели команду update:

  1. При этом игнорируются данные файла Cargo.lock. Производится поиск последних версий пакетов, описанных в файле Cargo.toml
  2. Если загрузка и компиляция зависимостей прошла успешно, происходит обновление данных файла Cargo.lock.

По умолчанию (согласно семантической теории версий), Cargo производит поиск новых версий пакета rand, которые большое 0.3.0 и меньше 0.4.0. Если существует несколько новых версий rand (0.3.15 и 0.4.0), при выполнении команды update вы увидите подобное сообщение:

$ cargo update
    Updating registry `https://github.com/rust-lang/crates.io-index`
    Updating rand v0.3.14 -> v0.3.15

В этому случае, данные файла Cargo.lock изменятся с строках, описывающие версию пакета.

Если же вы хотите перейти на версию 0.4.0 rand или даже большую 0.4.x, то в этом случае вам необходимо вручную поменять значение используемой версии пакета:

[dependencies]

rand = "0.4.0"

Все эти условности - следствия семантической теории версий пакетов. Пожалуйста, найдите время познакомиться с ней!

Когда Вы в очередной раз будете использовать команду cargo build, будет произведен анализ доступных версий исходя и новых особенностей зависимостей вашего проекта.

Более подробно о внутренней аналитической работе, связанной с анализом и обновлением версий пакетом можно прочитать на страницах документации и дополнительных статьях.

Благодаря активному использованию пакетов, создание ваших приложений становится проще, а проекты компактнее. Важно лишь знать о существовании нужного пакета.

Создание псевдослучайных чисел

Приступим к использованию функционала пакета rand. Когда необходимые пакеты на своих местах, можно менять исходный код программы src/main.rs:

Filename: src/main.rs

extern crate rand;

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

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

    let secret_number = rand::thread_rng().gen_range(1, 101);

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

Listing 2-3: В исходный код внесён функционал, создающий псевдослучайное число

Мы добавили строку extern crate rand;, которая сообщает Rust-компилятору, что далее будет использован функционал данного пакета. Префикс rand:: - это ссылка на данный пакет.

Далее мы добавляем строку кода use rand::Rng, где Rng - это т.н. типаж (элемент (синтаксическая конструкция) пакета rand, который определяет методы, которые реализуют генераторы псевдослучайных чисел). Этот типаж обязан быть в области видимости для выполняемого кода, для того, чтобы эти методы могли быть вызваны (при необходимости). Более подробно о типажах будет рассказано в Главе 10.

Кроме декларирования намерений, мы написали код, который использует вышеописанный функционал (если этого не сделать, то компилятор сообщит нам о нашей досадной забывчивости). Функция rand::thread_rng возвращает экземпляр генератора псевдослучайных чисел, ссылку на который мы используем в цепочке кода. Мы вызываем его метод gen_range для получения псевдослучайного числа. Этот метод определён в типаже Rng. Этот метод получает два числовых аргумента, которые являются концами числового отрезка. Второе число не входит в числовой отрезок, поэтому псевдослучайное число больше или равно 1 и строго меньше 101.

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

Строка кода:


# #![allow(unused_variables)]
#fn main() {
println!("The secret number is: {}", secret_number);
#}

печатает созданное случайное число. Вывод на печать текущих переменных - это одно из средств отладки программ. Но чтобы в создаваемую игру было интересно играть - данную строчку кода надо будет закомментировать (иначе нечего будет угадывать :-) ).

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

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

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 7
Please input your guess.
4
You guessed: 4
$ cargo run
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 83
Please input your guess.
5
You guessed: 5

Обратите внимание, что выбор числа на заданном отрезке будет случайный. Программа замечательно работает. Есть чему порадоваться. :-)

Реализация попыток угадывания загаданного числа

Теперь у нас есть код для введения данных игроком и загадывания числа (генерация псевдослучайного числа на заданном отрезке). Теперь осталось добавить реализацию анализа введённых данных (сравнить то, что ввёл игрок с тем, что сгенерировал пакет rand): (Listing 2-4):

Filename: src/main.rs

extern crate rand;

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

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

    let secret_number = rand::thread_rng().gen_range(1, 101);

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

Listing 2-4: Вывод на печать результатов сравнения чисел

Пожалуйста, закомментируйте новые строчки кода! Сделали? А теперь строка за строкой будем убирать комментарии и разбираться как это работает. Итак, первая строчка кода, которая была добавлена, как вы наверное догадались, находится в заголовочной части текста программы:

use std::cmp::Ordering;

Ordering - это перечисление, имеющее значения: Greater, Less и Equal. Всё логично, т.к. при любом сравнении исчисляемых объектов может быть только три этих результата: больше, меньше или равно.

Далее, в теле функции main мы добавили следующую синтаксическую конструкцию:

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

Метод cmp сравнивает два значения и может быть вызван любой значение, которое имеет такую функциональную возможность (возможность быть равной с чем-либо). Этот метод получает ссылку в качестве входного параметра. Метод возвращает значения перечисления Ordering. Это значение передаётся выражению match, благодаря которому выбирается действие, которое выполнится в зависимости от переданного значения.

Работа с этой оригинальной синтаксической конструкцией будет подробно описана в главах 6 и 18. Сейчас вы должны понять, что это весьма удобная конструкция решает поставленную перед нами задачу - по шаблону выполняется действие. Входные данные сравниваются со значением шаблона и выполняется соответствующий код программы.

Если теоретически такой код должен работать, проверим это на практике:

$ cargo build
   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 struct `std::string::String`, found integral variable
   |
   = note: expected type `&std::string::String`
   = note:    found type `&{integer}`

error: aborting due to previous error
Could not compile `guessing_game`.

Мы получаем ошибку несоответствия типа входных данных метода cmp. Rust не может сравнить величины разных типов (в данном случае строки и числа). guess неявно, при инициализации получил строковое значение. secret_number получил числовое значение.

Так как нам надо сравнить два числа, то строковое значение надо конвертировать в числовое. Следующий код демонстрирует такое преобразование:

Filename: src/main.rs

extern crate rand;

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

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

    let secret_number = rand::thread_rng().gen_range(1, 101);

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

Обратите, пожалуйста, внимание на две новые строки:

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

Обратите также внимание, на название числовой переменной, которую мы создали - guess! Мы создали переменную с тем же названием - для удобства и демонстрации функциональной возможности Rust - скрытие переменной. Да, таким образом можно скрывать переменные разных типов, чтобы не создавать новые. Такая вот интересная оптимизация на уровне языка программирования. В главе 3 вы узнаете об этом более подробно.

Мы связали переменную с именем guess с цепочкой вызовов функций. Остановимся подробнее на звеньях этой цепочки. Первым звеном является переменная guess, которая содержит текстовое представление числа, введённого с клавиатуры. Метод, trim, который был вызван далее удалил пробелы с начала и с конца введённой строки. Также этот метод удаляет все непечатаемые символы - такие, как знак табуляции, перенос на новую строку, возврат каретки.

Метод parse конвертирует текст в число. Т.к. данный метод позволяет получать разные числовые типы, мы должны явно указать, какой тип мы хотим получить. Задание типа переменной let guess: u32 - отличное решение для этой задачи. Тип u32 максимально близок к числам заданного отрезка. Вы познакомитесь со всеми числовыми типами в главе 3. Переменную secret_number Rust приведёт к типу данных u32 за нас.

Обратите внимание, что следующее звено в цепочке вызовов функций уже нам знакомо! Это функция expect. Это результат Result работы предыдущей функции. Если пользователь ввел нечисловое значение - будет сообщено об ошибке.

Давайте проверим работу нашей программы!

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
     Running `target/guessing_game`
Guess the number!
The secret number is: 58
Please input your guess.
  76
You guessed: 76
Too big!

Отлично! Обратите внимание, что мы ввели пробелы перед и после числа. Пожалуйста, проверьте все возможные варианты поведения программы!

Созданная нами программа может вести себя по-разному в зависимости от введённых данных. Какого функционала ей не хватает?! Пожалуйста, подумайте! Конечно, же многократных попыток угадать число!

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

loop даёт возможность организовать бесконечный цикл:

Filename: src/main.rs

extern crate rand;

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

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

    let secret_number = rand::thread_rng().gen_range(1, 101);

    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, да неправильный ввод данных помогут нам остановить работу программы:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
     Running `target/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/libcore/result.rs:785
note: Run with `RUST_BACKTRACE=1` for a backtrace.
error: Process didn't exit successfully: `target/debug/guess` (exit code: 101)

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

Прекращение цикла после введения правильного ответа

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

Filename: src/main.rs

extern crate rand;

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

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

    let secret_number = rand::thread_rng().gen_range(1, 101);

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

Если игрок введёт число равное загаданному - будет выведено поздравление и выполнен код (break;) прекращающий бесконечный цикл.

Обработка ошибок ввода

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

let guess: u32 = match guess.trim().parse() {
    Ok(num) => num,
    Err(_) => continue,
};

Заменяя вызов метода expect на выражение match мы заменяем аварийное завершение программы при ошибке конвертации на обработку этого события.

Пожалуйста, напишите код выводящий на печать результат работы функции parse:


let guess: u32 = match guess.trim().parse() {
    Ok(num) => num,
    Err(_) => continue,
};

Ok и Err - это значения перечисления Result. Функция parse возвращает одно из этих значений. Ok будет содержать в себе конвертированное числовое значение.

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

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

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
     Running `target/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!

Отлично! Благодаря этому улучшению кода наша программа замечательно работает. :-) Программа оттестирована и теперь можно или удалить код программы, который печатает секретное число или просто закомментировать его. Решайте сами. :-)

Вот финальный вид кода нашей программы:

Filename: src/main.rs

extern crate rand;

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

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

    let secret_number = rand::thread_rng().gen_range(1, 101);

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

Listing 2-5: Полный код программы "Угадай число"

Итоги

Поздравляю Вас! Вы успешно прошли все этапы создания игры. Поздравляю!

Это было обзорное представления возможностей и концепций языка Rust: ключевые слова let, match, методы, ассоциированные функции, внешние пакеты. Последующие главы расскажут вам об всем этом более подробно. Глава 3 рассказывает об общеязыковых концепциях Rust - переменные, типы данных, функции. Разбираются примеры их использования. Глава 4 повествует о владении. Это одна из отличительных черт Rust. Глава 5 рассказывает о структурах и их методах. Глава 6 рассказывает о перечислениях.