Угадайка

В качестве нашего первого проекта, мы решим классическую для начинающих программистов задачу: игра-угадайка. Немного о том, как игра должна работать: наша программа генерирует случайное целое число из промежутка от 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 <you@example.com>"]

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

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

fn main() { println!("Привет, мир!") }
fn main() {
    println!("Привет, мир!")
}

Давайте попробуем скомпилировать созданный 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) .ok() .expect("Не удалось прочитать строку"); println!("Ваша попытка: {}", guess); }
use std::io;

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

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

    let mut guess = String::new();

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

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

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

fn main() { use std::io; }
use std::io;

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

fn main() {
fn main() {

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

fn main() { println!("Угадайте число!"); println!("Пожалуйста, введите предположение."); }
    println!("Угадайте число!");

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

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

fn main() { let mut guess = String::new(); }
    let mut guess = String::new();

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

fn main() { let foo = bar; }
let foo = bar;

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

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

fn main() { let foo = 5; // неизменяемая связь let mut bar = 5; // изменяемая связь }
let foo = 5; // неизменяемая связь
let mut bar = 5; // изменяемая связь

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

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

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

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

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

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

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

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

fn main() { io::stdin() }
io::stdin()

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

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

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

fn main() { .read_line(&mut guess) }
.read_line(&mut guess)

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

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

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

Но мы пока что ещё не закончили с этой строкой кода. Пока это одна строка текста, это только первая часть одной логической строки кода:

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

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

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

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

Целью типов Result является преобразование информации об ошибках, полученных от обработчика. У значений типа Result, как и любого другого типа, есть определённые для него методы. В данном случае, у io::Result имеется метод ok(), который говорит, что «мы хотим получить это значение, если всё прошло хорошо. Если это не так, выбрось сообщение об ошибке». Но зачем выбрасывать? Для небольших программ, мы можем захотеть только вывести сообщение об ошибке и прекратить выполнение программы. Метод ok() возвращает значение, у которого объявлен другой метод: expect(). Метод expect() берёт значение, для которого он вызван, и если оно не удачное, выполняет panic! со строкой, заданной методу в качестве аргумента. panic! остановит нашу программу и выведет сообщение об ошибке.

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

$ 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 пытается сказать нам, что мы не обрабатываем ошибки, которые могут возникнуть. Наиболее правильным решением предотвращения ошибки будет её обработка. К счастью, если мы только хотим обрушить приложение, если есть проблема, мы можем использовать эти два небольших метода. Если мы можем восстановить что-либо из ошибки, мы должны сделать что-либо другое, но мы сохраним это для будущего проекта.

Там всего одна строка из первого примера:

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

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

fn main() { let x = 5; let y = 10; println!("x и y: {} и {}", x, y); }
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 — «библиотечный контейнер», который содержит код, предназначенный для использования с другими программами.

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

[dependencies]

rand="0.3.0"

Секция [dependencies] похожа на секцию [package]: всё, что расположено после объявления секции и до начала следующей, является частью этой секции. Cargo использует секцию с зависимостями чтобы знать о том, какие сторонние контейнеры потребуются, а так же какие их версии необходимы. В данном случае, мы используем версию 0.3.0. Cargo понимает семантическое версионирование, которое является стандартом нумерации версий. Если мы хотим использовать последнюю версию контейнера, мы можем использовать *. Так же мы можем указать необходимый промежуток версий. В документации 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 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.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) .ok() .expect("Не удалось прочитать строку"); println!("Ваша попытка: {}", guess); }
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)
        .ok()
        .expect("Не удалось прочитать строку");

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

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

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

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

fn main() { let secret_number = rand::thread_rng().gen_range(1, 101); println!("Загаданное число: {}", secret_number); }
    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) .ok() .expect("Не удалось прочитать строку"); println!("Ваша попытка: {}", guess); match guess.cmp(&secret_number) { Ordering::Less => println!("Слишком маленькое!"), Ordering::Greater => println!("Слишком большое!"), Ordering::Equal => println!("Вы выиграли!"), } }
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)
        .ok()
        .expect("Не удалось прочитать строку");

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

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

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

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

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

fn main() { enum Foo { Bar, Baz, } }
enum Foo {
    Bar,
    Baz,
}

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

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

fn main() { match guess.cmp(&secret_number) { Ordering::Less => println!("Слишком маленькое!"), Ordering::Greater => println!("Слишком большое!"), Ordering::Equal => println!("Вы выиграли!"), } }
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) .ok() .expect("Не удалось прочитать строку"); let guess: u32 = guess.trim().parse() .ok() .expect("Пожалуйста, введите число!"); println!("Ваша попытка: {}", guess); match guess.cmp(&secret_number) { Ordering::Less => println!("Слишком маленькое!"), Ordering::Greater => println!("Слишком большое!"), Ordering::Equal => println!("Вы выиграли!"), } }
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)
        .ok()
        .expect("Не удалось прочитать строку");

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

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

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

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

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

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

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

fn main() { guess.trim().parse() }
guess.trim().parse()

За которым следует вызов ok().expect(). Здесь 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(), мы будем использовать методы ok() и 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) .ok() .expect("Не удалось прочитать строку"); let guess: u32 = guess.trim().parse() .ok() .expect("Пожалуйста, введите число!"); println!("Ваша попытка: {}", guess); match guess.cmp(&secret_number) { Ordering::Less => println!("Слишком маленькое!"), Ordering::Greater => println!("Слишком большое!"), Ordering::Equal => println!("Вы выиграли!"), } } }
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)
            .ok()
            .expect("Не удалось прочитать строку");

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

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

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

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

$ 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) .ok() .expect("Не удалось прочитать строку"); let guess: u32 = guess.trim().parse() .ok() .expect("Пожалуйста, введите число!"); println!("Ваша попытка: {}", guess); match guess.cmp(&secret_number) { Ordering::Less => println!("Слишком маленькое!"), Ordering::Greater => println!("Слишком большое!"), Ordering::Equal => { println!("Вы выиграли!"); break; } } } }
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)
            .ok()
            .expect("Не удалось прочитать строку");

        let guess: u32 = guess.trim().parse()
            .ok()
            .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) .ok() .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; } } } }
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)
            .ok()
            .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;
            }
        }
    }
}

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

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

Здесь показано, как мы можем перейти от «сбоя при ошибке» к «обработке ошибки» заменив ok().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) .ok() .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; } } } }
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)
            .ok()
            .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, методы, привязанные функции, использование внешних контейнеров и многое другое. Наш следующий проект покажет ещё больше.