Rust #1 — «Угадай число», причесываем
Напомню, к чему мы пришли в прошлый раз.
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; } } }
Это работает — и как лобовой перевод кода более чем 30-летней давности даже довольно неплохо. Но выглядит это некрасиво и слишком загромождено низкоуровневыми деталями реализации. Обозначим себе фронт работ по рефакторингу:
- Избавиться от числовых констант
- Обработать возможные ошибки ввода и конвертации строки в число
Избавиться от констант
Констант у нас на первый взгляд две: минимальное и максимальное значение для интервала генерации случайного числа. Однако тут уместно поставить вопрос: мы пишем программу «угадай число от X до Y» или «угадай N-значное число»? Обратимся к исходному «этюду» — и там сказано «ЭВМ задумывает некоторое четырёхзначное число». Отлично, значит, будем исходить из этого.
Наивная попытка реализовать нужную функциональность будет выглядеть так.
const NUMBER_DIGITS: u32 = 4;
const LIMIT_LOWER: u32 = 10_u32.pow(NUMBER_DIGITS - 1);
const LIMIT_UPPER: u32 = 10_u32.pow(NUMBER_DIGITS);
Наивна она потому, что не компилируется. Тем не менее, попробуем разобраться с тем, что здесь используется (надеюсь, математика-то понятна без объяснений). Во-первых, объявление переменной (или константы, как в этом случае) может включать указание типа. Делается это после имени переменной через двоеточие. Знаковые целые числа — это i8…i128 (в зависимости от используемого числа бит), беззнаковые — u8…u128. Нет такого, как в C++, когда int имеет хрен знает сколько бит — это хорошо. С другой стороны, возникает соблазн заняться «оптимизацией», урезая биты там, где число должно вписаться в нужный интервал. На мой взгляд, делать этого не нужно. Если нет серьёзной причины, лучше использовать 32-битные числа.
Во-вторых, указывать тип можно (и временами нужно) не только для переменных, но и для числовых значений — через знак подчёркивания после числа.
В-третьих, pow хочет именно беззнаковый аргумент — и если ему на вход передать i32 без преобразования типов, обижается. Впрочем, это не что-то специфичное для Rust.
А теперь выясним, почему не компилируется. Ключевое слово const в Rust означает выражение, вычисляемое во время компиляции. Как constexpr в C++. А функция pow не так уж и проста в реализации — поэтому компилятор ругается, что пока она не может быть вычислена на этапе компиляции. Может быть, в будущей версии не будет ругаться — но здесь и сейчас надо сделать limit_lower и limit_upper обычными переменными.
let limit_lower: u32 = 10_u32.pow(NUMBER_DIGITS - 1);
let limit_upper: u32 = 10_u32.pow(NUMBER_DIGITS);От числовых констант мы избавились.
Обработать ошибки
На места, где это может быть нужно, указывает вызов метода unwrap: это чтение строки из stdin и преобразование её в число. Rust обрабатывает ошибки не выбрасывая исключения (я так понимаю, это медленно и сложно) и не возвращая какой-то код (который никто не проверяет). Вместо этого предлагается в случае ошибки, после которой выполнение не может быть продолжено, просто падать (panic), а в случае, если можно сделать какие-то осмысленные действия, возвращать тип Result, который либо оборачивает возвращаемое значение в Ok или Err — в зависимости от того, что именно произошло. Разумеется, тип возвращаемого значения может быть разным в зависимости от ситуации — обычно для ошибок создаётся собственный тип (как для исключений в C++). Но не будем забегать вперёд.
Не всё, что теоретически может быть как-то осмысленно обработано нуждается в такой обработке. Например, если мы не смогли вообще ничего прочитать из stdin, значит, произошло что-то глобально нехорошее, с чем вряд ли что-то может сделать наша маленькая числоугадайка. В этом случае паника может быть вполне логичным поведением — и именно это делает unwrap: либо возвращает значение, либо делает так, чтобы программа упала. Можно вывести своё сообщение об ошибке в дополнение к стандартному — тогда надо использовать expect, принимающий это сообщение как строковой параметр:
io::stdin().read_line(&mut guess).expect("Cannot read from stdin.");Впрочем, даже без этого Rust выдаёт довольно понятные сообщения об ошибке. Но если таких мест больше одного, разные сообщения об ошибке могут позволить понять, где именно она упала.
Однако если пользователь вводит неправильное число (например, отрицательное или слишком большое), либо что-то вообще левое, нам вовсе не обязательно паниковать — мы можем просто попросить повторить ввод. Такую ошибку можно и нужно обрабатывать. Для этого используем pattern matching в его довольно простой разновидности:
let guess: u32 = match guess.trim().parse() {
Ok(value) => value,
Err(_) => {
println!("Bad input, try again!");
continue;
},
};Подчёркивание в скобках после Err говорит о том, что нас не интересует, что там за ошибка произошла. Если бы мы написали там какое-нибудь имя (скажем, error), то можно было бы что-то сделать в зависимости от того, какая ошибка случилась, используя error как переменную. Многословно? Да, немного. Но зато все потенциальные ошибки как-то обработаны.
Итого у нас получается следующий код:
use rand::{thread_rng, Rng};
use std::io;
const NUMBER_DIGITS: u32 = 4;
fn main() {
let limit_lower: u32 = 10_u32.pow(NUMBER_DIGITS - 1);
let limit_upper: u32 = 10_u32.pow(NUMBER_DIGITS);
let number = thread_rng().gen_range(limit_lower, limit_upper);
let mut counter = 0;
loop {
counter += 1;
println!("Guess # {}", counter);
let mut guess = String::new();
io::stdin().read_line(&mut guess).expect("Cannot read from stdin.");
let guess: u32 = match guess.trim().parse() {
Ok(value) => value,
Err(_) => {
println!("Bad input, try again!");
continue;
},
};
if guess > number {
println!("Too much!");
} else if guess < number {
println!("Too little!");
} else {
println!("Got it!");
break;
}
}
}
И это всё ещё некрасиво и слишком загромождено низкоуровневыми деталями реализации. Вероятно, стоит попробовать выделить самостоятельные фрагменты этого кода в функции и добавить комментарии — но это материал для следующего поста.
Comments
Post a Comment