% Угадайка

В качестве нашего первого проекта мы решим классическую для начинающих программистов задачу: игра-угадайка. Немного о том, как игра должна работать: наша программа генерирует случайное целое число из промежутка от 1 до 100. Затем она просит ввести число, которое она «загадала». Для каждого введённого нами числа, она говорит, больше ли оно, чем «загаданное», или меньше. Игра заканчивается когда мы отгадываем число. Звучит неплохо, не так ли?

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

Давайте создадим новый проект. Перейдите в вашу директорию с проектами. Помните, как мы создавали структуру директорий и Cargo.toml для hello_world? Cargo может сделать это за нас. Давайте воспользуемся этим:

$ cd ~/projects
$ cargo new guessing_game --bin
$ cd guessing_game

Мы сказали Cargo, что хотим создать новый проект с именем guessing_game. При помощи флага --bin мы указали, что хотим создать исполняемый файл, а не библиотеку.

Давайте посмотрим сгенерированный Cargo.toml:

[package]

name = "guessing_game"
version = "0.1.0"
authors = ["Your Name <[email protected]>"]

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

Наконец, Cargo создал программу «Привет, мир!». Загляните в файл src/main.rs:

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

Давайте попробуем скомпилировать созданный Cargo проект:

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

Замечательно! Снова откройте src/main.rs. Мы будем писать весь наш код в этом файле.

Прежде чем мы начнём работу, давайте рассмотрим ещё одну команду Cargo: run. cargo run похожа на cargo build, но после завершения компиляции она запускает получившийся исполняемый файл:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///home/you/projects/guessing_game)
     Running `target/debug/guessing_game`
Привет, мир!

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

Обработка предположения

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

use std::io;

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

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

    let mut guess = String::new();

    io::stdin().read_line(&mut guess)
        .expect("Не удалось прочитать строку");

    println!("Ваша попытка: {}", guess);
}

Здесь много чего! Давайте разберём этот участок по частям.

use std::io;

Нам надо получить то, что ввёл пользователь, а затем вывести результат на экран. Значит нам понадобится библиотека io из стандартной библиотеки. Изначально Rust импортирует в нашу программу лишь самые необходимые вещи — прелюдию (prelude). Если чего-то нет в прелюдии, мы должны при помощи use явно указать, что хотим это использовать. Здесь также присутствует вторая прелюдия — io-прелюдия; когда вы её импортируете, она подключает ряд полезных вещей, связанных с io.

fn main() {

Как вы уже знаете, функция main() — это точка входа в нашу программу. fn объявляет новую функцию. Пустые круглые скобки () показывают, что она не принимает аргументов. Открывающая фигурная скобка { начинает тело нашей функции. Из-за того, что мы не указали тип возвращаемого значения, предполагается, что будет возвращаться () — пустой кортеж.

    println!("Угадайте число!");

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

Мы уже изучили, что println!() — это макрос, который выводит строки на экран.

    let mut guess = String::new();

Теперь интереснее! Как же много всего происходит в этой строке! Первая вещь, на которую следует обратить внимание, — выражение let, которое используется для создания связи. Оно выглядит так:

let foo = bar;

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

Например, по умолчанию, связи неизменяемы. По этой причине наш пример использует mut: этот модификатор разрешает менять связь. С левой стороны от присваивания у let может быть не просто имя связи, фактически он принимает образец. Мы будем использовать их дальше. Их достаточно просто использовать:

let foo = 5; // неизменяемая связь
let mut bar = 5; // изменяемая связь

Ах да, // начинает комментарий, который заканчивается в конце строки. Rust игнорирует всё, что находится в комментариях.

Теперь мы знаем, что let mut guess объявляет изменяемую связь с именем guess, а по другую сторону от = находится то, что будет привязано: String::new().

String — это строковый тип, предоставляемый нам стандартной библиотекой. String — это текст в кодировке UTF-8 переменной длины.

Синтаксис ::new() использует ::, так как это привязанная к определённому типу функция. То есть она привязана к самому типу String, а не к определённой переменной типа String. Некоторые языки называют это «статическим методом».

Имя этой функции — new(), так как она создаёт новый, пустой экземпляр типа String. Вы можете найти эту функцию у многих типов, потому что это общее имя для создания нового значения определённого типа.

Давайте посмотрим дальше:

    io::stdin().read_line(&mut guess)
        .expect("Не удалось прочитать строку");

Это уже побольше! Давайте это всё разберём. В первой строке есть две части. Это первая:

io::stdin()

Помните, как мы импортировали (use) std::io в самом начале нашей программы? Сейчас мы вызвали ассоциированную с ним функцию. Если бы мы не сделали use std::io, нам бы пришлось здесь написать std::io::stdin().

Эта функция возвращает обработчик стандартного ввода нашего терминала. Более подробно об этом можно почитать в std::io::Stdin.

Следующая часть использует этот обработчик для получения всего, что введёт пользователь:

.read_line(&mut guess)

Здесь мы вызвали метод read_line() обработчика. Методы похожи на ассоциированные функции, но доступны только у определённого экземпляра типа, а не самого типа. Мы указали один аргумент функции read_line(): &mut guess.

Помните, как мы выше привязали guess? Мы сказали, что эта ссылка изменяема. Однако read_line не получает в качестве аргумента String: она получает &mut String. В Rust есть такая особенность, называемая «ссылки», которая позволяет нам иметь несколько ссылок на одни и те же данные, что позволяет избежать излишнего их копирования. Ссылки — достаточно сложная особенность, и одним из основных подкупающих достоинств Rust является то, как он решает вопрос безопасности и простоты их использования. Пока что для завершения программы нам не требуется разбираться в таких подробностях. Сейчас всё, что нам нужно, — это знать, что ссылки, как и связывание при помощи let, неизменяемы по умолчанию. Следовательно, мы должны написать &mut guess, а не &guess.

Почему read_line() получает изменяемую ссылку на строку? Его работа — это взять то, что пользователь написал в стандартный ввод, и положить это в строку. Итак, метод получает строку в качестве аргумента, и для того, чтобы поместить в неё введённое значение, ссылка на данную строку должна быть изменяемой.

Мы, однако, ещё не вполне разобрались с этой строкой кода. Хотя рассмотренный фрагмент программы — просто одна строка её текста, он является лишь первой частью одной логической строки кода. Посмотрим далее:

        .expect("Не удалось прочитать строку");

Когда мы вызываем метод, используя синтаксис .foo(), мы можем перенести вызов в новую строку и сделать для него отступ. Это помогает разбивать длинные строки на несколько. Мы могли бы сделать и так:

    io::stdin().read_line(&mut guess).expect("Не удалось прочитать строку");

Однако это достаточно трудно читать. Поэтому мы разделили строку: по строке на каждый вызов метода. Мы уже поговорили о read_line(), но ещё ничего не сказали про expect(). Мы узнали, что read_line() помещает введённые пользователем данные в ссылку &mut String, которую мы передали методу в качестве аргумента. Но данный метод также возвращает значение: в данном случае это экземпляр типа io::Result. В стандартной библиотеке Rust есть несколько типов с именем Result: общая версия Result и несколько отдельных версий в подбиблиотеках, например io::Result.

Назначение типов Result — преобразовывать информацию об ошибках, полученных от обработчика. У значений типа Result, как и любого другого типа, есть определённые для него методы. Так, у io::Result имеется метод expect(), который берёт значение, для которого был вызван этот метод, и, если оно неудачное, выполняет panic! со строкой, переданной методу в качестве аргумента. panic! остановит нашу программу и выведет сообщение об ошибке.

Если убрать вызовы этих двух методов, наша программа скомпилируется, но компилятор выдаст следующее предупреждение:

$ cargo build
   Compiling guessing_game v0.1.0 (file:///home/you/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);
                   ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Rust предупреждает, что мы не используем значение Result. Это предупреждение пришло из специальной аннотации, которая указана в io::Result. Rust пытается сказать нам, что мы не обрабатываем ошибки, которые могут возникнуть. Наиболее правильным решением предотвращения ошибки будет её обработка. К счастью, если мы только хотим обрушить приложение, когда возникает проблема, мы можем использовать эти два небольших метода. Если мы можем восстановить что-либо из ошибки, мы должны сделать кое-что другое, но мы оставим это для будущего проекта.

Осталось разобрать всего одну строку из первого примера:

    println!("Ваша попытка: {}", guess);
}

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

let x = 5;
let y = 10;

println!("x и y: {} и {}", x, y);

Просто.

Мы можем запустить то, что у нас есть, при помощи cargo run:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///home/you/projects/guessing_game)
     Running `target/debug/guessing_game`
Угадайте число!
Пожалуйста, введите предположение.
6
Ваша попытка: 6

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

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

Далее, нам надо сгенерировать секретное число. В стандартной библиотеке Rust нет ничего, что могло бы нам предоставить функционал для генерации случайных чисел. Однако разработчики Rust для этого предоставили контейнер (crate) rand. «Контейнер» — это пакет с кодом Rust. Наш проект — «бинарный контейнер», из которого в итоге получится исполняемый файл. rand — «библиотечный контейнер», который содержит код, предназначенный для использования с другими программами.

Использование внешних контейнеров — задача для Cargo. Прежде чем мы начнём писать код с использованием rand, мы должны модифицировать наш Cargo.toml. Откроем его и добавим в конец следующие строчки:

[dependencies]

rand="0.3.0"

Раздел [dependencies] в Cargo.toml похож на раздел [package]: к нему относится всё то, что помещено после строки объявления, вплоть до следующего раздела. Cargo использует раздел зависимостей, чтобы знать, какие сторонние контейнеры потребуются, а также какие их версии необходимы. В данном случае мы используем версию 0.3.0, под которой Cargo подразумевает любую версию, совместимую с заданной. Cargo понимает Семантическое версионирование, которое является стандартом нумерации версий. Простое указание номера, как показано выше, является сокращением для ^0.3.0, и обозначает «все версии, совместимые с 0.3.0». Если мы хотим использовать только 0.3.0, то можно написать rand="=0.3.0" (обратите внимание на два знака равенства). А если мы хотим использовать последнюю версию контейнера, мы можем использовать *. Также мы можем указать необходимый промежуток версий. В документации Cargo вы найдёте больше информации.

Теперь, не совершив никаких изменений в коде нашей программы, давайте соберём проект:

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

(Конечно же, вы можете увидеть другие номера версий.)

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

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

Когда мы запустим cargo build снова, текст вывода будет уже другим:

$ cargo build

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

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

Итак, мы сказали Cargo, что нам нужна библиотека rand с любой версией ветки 0.3.x, и он взял последнюю версию на тот момент, когда его запустили, — v0.3.8. Но что делать, когда на следующей неделе выйдет версия v0.3.9, содержащая важные изменения? Что, если исправления настолько масштабны, что версия 0.3.9 становится несовместимой с нашим кодом?

Решением этой проблемы является файл Cargo.lock, который находится в директории с нашим проектом. Когда мы в первый раз собирали наш проект, Cargo подобрал версии, подходящие под наши условия, и записал их в файл Cargo.lock. Когда мы в будущем будем собирать наш проект, Cargo будет проверять, существует ли Cargo.lock, и затем использовать указанные в нём версии контейнеров. Благодаря этому мы автоматически получаем повторяемые сборки. Другими словами, мы будем использовать контейнер версии 0.3.8 до тех пор, пока явно не обновим информацию о его версии в Cargo.lock.

А что, если мы захотим использовать версию v0.3.9? У Cargo есть другая команда, update, которая скажет Cargo: «Игнорируй Cargo.lock, найди и загрузи последние версии библиотек из веток, указанных в Cargo.toml. Когда всё сделаешь, запиши информацию о версиях в Cargo.lock». По умолчанию Cargo ищет только такую версию, номер которой больше, чем 0.3.0, и меньше, чем 0.4.0. Если мы хотим перейти на версии 0.4.x, мы должны указать это в Cargo.toml. Потом, когда мы запустим cargo build, Cargo обновит индекс и пересмотрит наши требования к rand.

В документации по Cargo можно узнать намного больше как о нём, так и о его экосистеме, но сказанного выше нам пока хватит. Cargo делает повторное использование библиотек намного проще, и программисты на Rust, как правило, пишут небольшие проекты, которые входят в состав других, более крупных, проектов.

Давайте наконец использовать rand. Вот наш следующий шаг:

extern crate rand;

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

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

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

    println!("Загаданное число: {}", secret_number);

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

    let mut guess = String::new();

    io::stdin().read_line(&mut guess)
        .expect("Не удалось прочитать строку");

    println!("Ваша попытка: {}", guess);
}

Первое, что мы сделали, — изменили первую строку. Теперь она выглядит так: extern crate rand. Так как мы указали rand в разделе [dependencies], мы можем использовать extern crate для того, чтобы Rust знал, что мы собираемся использовать эту зависимость. extern crate также выполняет эквивалент оператора use rand;, т.е. теперь мы можем использовать всё, что есть в контейнере rand, используя префикс rand::.

Далее, мы добавили новую строку use: use rand::Rng. Мы собираемся использовать метод, а ему нужно, чтобы Rng был в области видимости. Основная идея такова: методы, объявленные где-то в другом месте, называются «типажами» (traits), и для того, чтобы этот метод можно было использовать, необходимо иметь типаж в области видимости. Чтобы узнать об этом более подробно, вы можете прочитать раздел о типажах.

Мы добавили две новые строки в середину кода:

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

    println!("Загаданное число: {}", secret_number);

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

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

Давайте запустим изменённую программу:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///home/you/projects/guessing_game)
     Running `target/debug/guessing_game`
Угадайте число!
Загаданное число: 7
Пожалуйста, введите предположение.
4
Ваша попытка: 4
$ cargo run
     Running `target/debug/guessing_game`
Угадайте число!
Загаданное число: 83
Пожалуйста, введите предположение.
5
Ваша попытка: 5

Замечательно! Следующий шаг — сравнение нашего предположения с «загаданным» числом.

Сравнение

Теперь, когда мы знаем, что ввёл пользователь, давайте сравним «загаданное» число с предполагаемым ответом. Здесь приведён наш следующий шаг, который, к сожалению, не будет работать:

extern crate rand;

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

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

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

    println!("Загаданное число: {}", secret_number);

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

    let mut guess = String::new();

    io::stdin().read_line(&mut guess)
        .expect("Не удалось прочитать строку");

    println!("Ваша попытка: {}", guess);

    match guess.cmp(&secret_number) {
        Ordering::Less    => println!("Слишком маленькое!"),
        Ordering::Greater => println!("Слишком большое!"),
        Ordering::Equal   => println!("Вы выиграли!"),
    }
}

Здесь мы видим что-то новое. Первое — это ещё один use. Мы ввели в область видимости тип std::cmp::Ordering. Далее, ещё пять новых строк в конце, которые используют его:

match guess.cmp(&secret_number) {
    Ordering::Less    => println!("Слишком маленькое!"),
    Ordering::Greater => println!("Слишком большое!"),
    Ordering::Equal   => println!("Вы выиграли!"),
}

Метод cmp() может быть вызван у чего-либо, что может сравниваться, и получает в качестве аргумента ссылку на то, с чем мы хотим его сравнить. Результатом сравнения будет значение типа Ordering, который мы импортировали выше. Мы используем оператор match для определения Ordering — результата сравнения. Ordering — перечисление. Они обозначаются enum, сокращённо от enumeration (перечисление). Перечисления выглядят следующим образом:

enum Foo {
    Bar,
    Baz,
}

С таким определением всё, что имеет тип Foo может иметь значение либо Foo::Bar, либо Foo::Baz. Мы используем :: для обозначения пространства имён для вариантов перечисления.

У перечисления Ordering есть три возможных варианта: Less, Equal и Greater. Выражение match получает переменную какого-либо типа и предлагает вам создать «ветви» для каждого возможного значения. Так как у нас есть три возможных значения Ordering, у нас будет три ветви:

match guess.cmp(&secret_number) {
    Ordering::Less    => println!("Слишком маленькое!"),
    Ordering::Greater => println!("Слишком большое!"),
    Ordering::Equal   => println!("Вы выиграли!"),
}

Если результатом сравнения будет значение Less, мы выведем на экран Слишком маленькое!; если будет Greater, то Слишком большое!; и если Equal, то Вы выиграли!. match очень удобен, и он часто используется в Rust.

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

$ cargo build
   Compiling guessing_game v0.1.0 (file:///home/you/projects/guessing_game)
src/main.rs:28:21: 28:35 error: mismatched types:
 expected `&collections::string::String`,
    found `&_`
(expected struct `collections::string::String`,
    found integral variable) [E0308]
src/main.rs:28     match guess.cmp(&secret_number) {
                                   ^~~~~~~~~~~~~~
error: aborting due to previous error
Could not compile `guessing_game`.

У-у-у! Это большая ошибка. Суть этой ошибки в «несоответствии типов» (mismatched types). В Rust строгая статическая система типов. Но в нём также есть вывод типов. Когда мы пишем let guess = String::new(), Rust понимает, что guess должна быть типа String, благодаря чему мы можем не указывать тип явно. secret_number — число, которое может иметь значение от одного до ста. Оно может иметь тип i32 — 32-битное целое, или u32 — 32-битное целое без знака, или i64 — 64-битное целое, или какой-нибудь другой. По умолчанию, Rust сделает его 32-битным целым, i32. Однако здесь Rust не знает, как сравнить guess и secret_number. Они должны быть одного типа. В итоге, чтобы можно было сравнить guess и secret_number, мы должны преобразовать переменную guess, которую мы прочитали с ввода, из типа String в настоящий числовой тип. Мы можем сделать это, добавив несколько строчек. Вот как будет выглядеть наша программа:

extern crate rand;

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

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

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

    println!("Загаданное число: {}", secret_number);

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

    let mut guess = String::new();

    io::stdin().read_line(&mut guess)
        .expect("Не удалось прочитать строку");

    let guess: u32 = guess.trim().parse()
        .expect("Пожалуйста, введите число!");

    println!("Ваша попытка: {}", guess);

    match guess.cmp(&secret_number) {
        Ordering::Less    => println!("Слишком маленькое!"),
        Ordering::Greater => println!("Слишком большое!"),
        Ordering::Equal   => println!("Вы выиграли!"),
    }
}

Вот строки, которые мы добавили:

    let guess: u32 = guess.trim().parse()
        .expect("Пожалуйста, введите число!");

Подождите минутку, у нас ведь уже есть guess? Rust позволил нам «затенить» (скрыть) предыдущее guess новым. Это часто используется в подобных случаях, когда guess изначально бывает типа String, но нам требуется преобразовать её в u32. Затенение позволяет нам переиспользовать имя guess вместо того, чтобы создавать для значений разных типов уникальные имена (такие как guess_str и guess).

Мы связали guess с выражением, похожим на написанное когда-то ранее:

guess.trim().parse()

Здесь guess ссылается на старый guess, который ещё является строкой, полученной нами с ввода. Метод trim() у типа String удаляет все пробелы из начала и конца нашей строки. Это важно, ведь для нормальной работы read_line() нам необходимо нажать Enter по окончании ввода. Это значит, что если мы наберём 5 и нажмём Enter, то guess будет выглядеть следующим образом: 5\n. Последовательность \n обозначает «новую строку» (newline) — значение клавиши Enter. trim() удалит её и оставит только 5. Метод parse(), применяемый к строке, преобразует её в число. Он может анализировать различные числа, но мы можем указать Rust, какой именно тип нам нужен. Поэтому мы указали let guess: u32. Двоеточие :, идущее после guess, говорит Rust, что мы указываем тип значения. u32 — 32-битное беззнаковое целое число. В Rust встроено несколько числовых типов, но мы выбрали именно u32. Это достаточно хороший тип, чтобы хранить небольшие положительные числа.

Как и read_line(), вызов parse() может вызвать проблемы. Что, если наша строка будет содержать A👍%? Мы не сможем преобразовать её в число. Как и в случае с read_line(), мы будем использовать метод expect() на случай, если parse() не сможет преобразовать строку.

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

$ cargo run
   Compiling guessing_game v0.1.0 (file:///home/you/projects/guessing_game)
     Running `target/guessing_game`
Угадайте число!
Загаданное число: 58
Пожалуйста, введите предположение.
  76
Ваша попытка: 76
Слишком большое!

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

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

Зацикливание

Ключевое слово loop создаёт бесконечный цикл. Давайте добавим его:

extern crate rand;

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

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

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

    println!("Загаданное число: {}", secret_number);

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

        let mut guess = String::new();

        io::stdin().read_line(&mut guess)
            .expect("Не удалось прочитать строку");

        let guess: u32 = guess.trim().parse()
            .expect("Пожалуйста, введите число!");

        println!("Ваша попытка: {}", guess);

        match guess.cmp(&secret_number) {
            Ordering::Less    => println!("Слишком маленькое!"),
            Ordering::Greater => println!("Слишком большое!"),
            Ordering::Equal   => println!("Вы выиграли!"),
        }
    }
}

И посмотрим на работу приложения. Но подождите, мы же добавили бесконечный цикл? Всё верно. Помните, что мы говорили о parse()? Если мы введём не числовой ответ, то просто вызовем panic! и выйдем из программы. Посмотрите:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///home/you/projects/guessing_game)
     Running `target/guessing_game`
Угадайте число!
Загаданное число: 59
Пожалуйста, введите предположение.
45
Ваша попытка: 45
Слишком маленькое!
Пожалуйста, введите предположение.
60
Ваша попытка: 60
Слишком большое!
Пожалуйста, введите предположение.
59
Ваша попытка: 59
Вы выиграли!
Пожалуйста, введите предположение.
quit
thread '<main>' panicked at 'Пожалуйста, введите число!'

Ха! Если мы введём quit, то действительно выйдем из программы. Как и при вводе любого другого не числового значения. Что ж, это, мягко говоря, не очень хорошо. Для начала давайте сделаем выход из программы, когда мы выиграли игру:

extern crate rand;

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

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

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

    println!("Загаданное число: {}", secret_number);

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

        let mut guess = String::new();

        io::stdin().read_line(&mut guess)
            .expect("Не удалось прочитать строку");

        let guess: u32 = guess.trim().parse()
            .expect("Пожалуйста, введите число!");

        println!("Ваша попытка: {}", guess);

        match guess.cmp(&secret_number) {
            Ordering::Less    => println!("Слишком маленькое!"),
            Ordering::Greater => println!("Слишком большое!"),
            Ordering::Equal   => {
                println!("Вы выиграли!");
                break;
            }
        }
    }
}

С добавлением строки break после вывода Вы выиграли! мы получили возможность выхода из цикла, когда мы угадали загаданное число. Выход из цикла здесь также означает и завершение нашей программы, так как цикл — это последнее, что есть в main(). Нам надо сделать ещё одно улучшение — при любом нечисловом вводе мы не должны выходить из программы, мы просто должны проигнорировать ввод. Мы можем сделать это следующим образом:

extern crate rand;

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

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

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

    println!("Загаданное число: {}", secret_number);

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

        let mut guess = String::new();

        io::stdin().read_line(&mut guess)
            .expect("Не удалось прочитать строку");

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

        println!("Ваша попытка: {}", guess);

        match guess.cmp(&secret_number) {
            Ordering::Less    => println!("Слишком маленькое!"),
            Ordering::Greater => println!("Слишком большое!"),
            Ordering::Equal   => {
                println!("Вы выиграли!");
                break;
            }
        }
    }
}

Вот строка, которую мы изменили:

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

Здесь показано, как мы можем перейти от «сбоя при ошибке» к «обработке ошибки», заменив expect() на инструкцию match. Result, возвращённый функцией parse(), как и Ordering, является перечислением. Однако в данном случае каждый из вариантов имеет некоторые ассоциированные с ним данные: Ok — успех, Err — ошибку. У каждого есть некоторая дополнительная информация: преобразованное число, либо тип ошибки. Здесь мы проверили значение результата работы parse() при помощи match. В случае, если результат равен Ok, то match привяжет внутреннее значение результата (Ok(num)) к имени num и вернёт в привязку guess. Когда происходит ошибка (Err), нам не важно, какая именно это ошибка, поэтому мы используем вместо имени _. Так мы проигнорируем ошибку и вызовем continue, что отправит нас на следующую итерацию цикла.

Теперь всё должно быть нормально! Давайте посмотрим:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///home/you/projects/guessing_game)
     Running `target/guessing_game`
Угадайте число!
Загаданное число: 61
Пожалуйста, введите предположение.
10
Ваша попытка: 10
Слишком маленькое!
Пожалуйста, введите предположение.
99
Ваша попытка: 99
Слишком большое!
Пожалуйста, введите предположение.
foo
Пожалуйста, введите предположение.
61
Ваша попытка: 61
Вы выиграли!

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

extern crate rand;

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

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

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

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

        let mut guess = String::new();

        io::stdin().read_line(&mut guess)
            .expect("Не удалось прочитать строку");

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

        println!("Ваша попытка: {}", guess);

        match guess.cmp(&secret_number) {
            Ordering::Less    => println!("Слишком маленькое!"),
            Ordering::Greater => println!("Слишком большое!"),
            Ordering::Equal   => {
                println!("Вы выиграли!");
                break;
            }
        }
    }
}

Готово!

Вы сделали «Угадайку»! Поздравляем!

Этот первый проект показал вам многое: let, match, методы, ассоциированные функции, использование внешних контейнеров и многое другое. Наш следующий проект покажет ещё больше.

results matching ""

    No results matching ""