Rust #0 — «Угадай число», первая версия
Что и зачем
Небольшая попытка научить старую собаку новым трюкам, подновить заржавевшие
навыки программирования — через освоение основ языка Rust.
Установка Rust
Я использую Windows 10. Идём на сайт
https://www.rust-lang.org/learn/get-started и качаем rustup. По дефолту
устанавливает в домашний каталог, чего я не люблю. Поэтому делаем
SET CARGO_HOME=c:\tools\rust\cargo
SET RUSTUP_HOME=C:\tools\rust\rustup
(разумеется, заменив пути на удобные себе) и только потом запускаем
установщик. Менять настройки на данном этапе смысла нет. Переменные окружения
выше позже имеет смысл прописать в системных настройках (или ничего без них не
заработает).
Ресурсы для справки
https://fasterthanli.me/articles/a-half-hour-to-learn-rust
— короткий вводный курс на примерах
https://doc.rust-lang.org/rust-by-example/
— более подробный курс на примерах
https://doc.rust-lang.org/book/title-page.html
— The Fucking Manual
https://doc.rust-lang.org/std/index.html
— ещё один The Fucking Manual
Что будем делать для первого раза
Задача, которую мы будем решать, взята из книги «24 этюда на Бейсике» В.Ф.
Очкова и Ю.В. Пухначёва. Для меня она имеет определённое символическое
значение: в далёком 1990-м именно её семилетний я вбивал в гудящую
вентиляторами Искру-226. Разумеется, тогда это воспринималось как своего
рода магическое заклинание. Но тем не менее, я вводил программу, а
компьютер её исполнял. Тогда, разумеется, всё было намного проще.
Исходный код на Бейсике выглядел следующим образом.
Для начала попробуем перенести его на Rust практически «в лоб», а потом
уже будем немного усовершенствовать. И да, я знаю, что в книжках по
ссылкам выше есть пример написания точно такой игры. Но с ним я буду
сверять результат своих действий после того, как закончу.
Создаём проект
cargo new guess_number
Делает нам hello world.
cargo build
cargo run
Запускает. Кроме того, автоматически создаётся git-репозиторий для
проекта, ничего специально делать не надо. Исходник лежит в src/main.rs.
Случайное число
Начинаем с первой строчки. Нам нужно получить случайное число от 1000 до
9999. В Бейсике за это обычно отвечала встроенная функция RND, возвращавшая
результат работы генератора случайных чисел (ГСЧ) в диапазоне от 0 до 1.
Разумеется, не всё было так просто, но углубляться во всякие RANDOMIZE
сейчас бессмысленно. В Rust генератора случайных чисел, встроенного в язык…
нет. Довольно удивительно не найти в современном языке нечто,
присутствовавшее в любом древнем Бейсике, правда?
Придётся познакомиться с менеджером пакетов. За ГСЧ в Rust отвечает «crate»
rand — её надо включить в список зависимостей пакета, указав конкретную
используемую версию (чтобы не ломалось при обновлении в апстриме). Для этого
в файле Cargo.toml в секции [dependencies] добавляем строку:
rand = "0.7"
А теперь приготовьтесь. Увы, сделать такую довольно элементарную вещь
«просто» в 2020 году не получится, в отличие от 1990-го. Во-первых, в начало
исходника надо добавить следующее магическое заклинание:
use rand::{thread_rng, Rng};
Rust, увы, не для новичков или слабых духом. Ну ладно, общий смысл понятен:
нынче принято из стороннего пакета брать только нужный минимум. И если смысл
импортирования thread_rng ещё более-менее понятен (это функция, которая
возвращает нам объект ГСЧ), то вот с Rng сложнее. Чтобы более-менее понять,
пришлось читать руководство: это «трейт», то есть по сути интерфейс, который
может быть реализован каким-то объектом, в нашем случае ГСЧ. Что-то типа
абстрактного класса. Слава богу, дальше проще: для генерации числа в нужном
диапазоне завезли готовую функцию. Поэтому пишем первой строчкой главной
функции:
let number = thread_rng().gen_range(1000,10000);
println!("{}", number);
Это позволит нам убедиться, что программа генерирует случайные числа в
нужном диапазоне, причём каждый раз разные — слава богу, RANDOMIZE тут не
нужен. Пока обойдёмся без юнит-тестов, не всё сразу же.
Запускаем описанным выше способом. cargo build сам скачает нужные пакеты…
откуда-то. Неплохо бы в этом месте сделать коммит, всё-таки мы уже что-то
делаем отличное от hello world.
Счётчик
С ним в целом всё просто и почти как в Бейсике.
let mut counter = 0;
Для нашего простого случая вполне работает автоматический вывод типов. Но
нужно указать, что мы планируем обновлять значение счётчика ключевым словом
mut — а вот случайное число мы определяем один раз на всё время исполнения
программы. И нет, это не константа (в терминах Rust) — константы могут быть
только вычисляемыми во время компиляции.
Цикл
Разумеется, мы не будем использовать GOTO (я даже не знаю, есть ли оно в
Rust, скорее всего, нет). В примере на Бейсике реализован рудиментарный
игровой цикл, который обычно реализуется через while(true) с break на
условии выхода из игры. В Rust для этого есть цикл loop, который умеет как
раз то, что нам нужно и ещё кое-что. Для начала, просто запишем пустой цикл:
loop {
}
Вывод и ввод
Первое, что мы делаем в цикле — увеличиваем счётчик на 1 и выводим номер
попытки.
counter += 1; println!("Guess # {}", counter);
Ничего необычного здесь нет. С println! мы уже имели дело выше, для наших
целей пока достаточно знать, что этот макрос (на что указывает восклицательный
знак) печатает строку, подставляя по порядку аргументы на место фигурных
скобок. Вероятно, здесь есть куча нюансов, как в printf из C, но для этой
программы они не нужны. С выводам разобрались. А вот со вводом будет чуть
посложнее. Во-первых, надо сказать, что мы будем использовать функции
ввода-вывода из стандартной библиотеки. Добавляем в начало программы
use std::io;
Во-вторых, надо создать переменную и прочитать в неё строку из stdin. Сделать
это можно так:
let mut guess = String::new(); io::stdin().read_line(&mut guess);
В принципе, это работает, но компилятор ругается на то, что мы не обработали
результат чтения строки — вдруг у нас stdin кончился. Для начала
давайте решим, что нас совершенно не волнует, что программа в этом случае
будет просто падать с тем сообщением об ошибке, которое за нас составит
компилятор, и просто допишем .unwrap():
io::stdin().read_line(&mut input).unwrap();
Предупреждение компилятора исчезнет. Но пока что мы прочитали строку, а нам
нужно число. И нет, метода для чтения числа в стандартной библиотеке нет. Зато
у строк есть функции trim и parse, которые позволяют очистить её от ненужных
пробелов и прочих «пустых» символов на концах, и конвертировать её в число.
let guess: u32 = guess.trim().parse().unwrap();
Обратить внимание здесь можно на несколько вещей. Во-первых, мы
затеняем переменную guess — нам больше не нужно её старое строковое
значение, с этого момента мы будем работать с ней как с числом. Во-вторых, мы
явно указываем тип, чтобы компилятор знал, как парсить строку. В-третьих,
здесь тоже возможны ошибки, и их мы тоже игнорируем.
Проверка числа
Слава богу, оператор if в Rust работает примерно так же, как и в других
языках, а break выходит из цикла. Поэтому пишем без сложностей:
if guess > number { println!("Too much!"); } else if guess < number { println!("Too little!"); } else { println!("Got it!"); break; }
И всё, это работает.
Результат
Получается вот такой код.
use rand::{thread_rng, Rng}; use std::io; fn main() { let number = thread_rng().gen_range(1000,10000); let mut counter = 0; loop { counter += 1; println!("Guess # {}", counter); let mut guess = String::new(); io::stdin().read_line(&mut guess).unwrap(); let guess: u32 = guess.trim().parse().unwrap(); if guess > number { println!("Too much!"); } else if guess < number { println!("Too little!"); } else { println!("Got it!"); break; } } }
Работает ли он? Да, работает. Но выглядит даже хуже эквивалента на Бейсике образца 1988 года. Да и некоторые углы мы срезали, а делать это, возможно, не стоило. Например, вполне можно правильно обработать ввод неправильного числа. Но этим я займусь в следующий раз.
Comments
Post a Comment