Уроки · 79
Option: значения может не быть
Давай перепишем поиск из урока «Поиск ближайшей цели» так, чтобы в нём было меньше места для ошибки. Турель будет вести себя точно так же. Изменится только то, как мы записываем «цель нашлась» и «цели нет».
Что не так с флагом
Заголовок раздела «Что не так с флагом»В том уроке у нас были две переменные: found и best_id, сначала
равная нулю. Пока цель не найдена, в best_id лежит ноль, но это не ID
какой-то мишени, а просто число, чтобы переменная чем-то была. Помнить,
что на best_id можно смотреть только когда found равно true,
приходится самому. Забудешь проверить, и программа возьмёт мишень с ID
0, а такой может и не быть, тогда она упадёт.
Для такого случая в Rust есть тип Option. Он значит «значение, которого
может и не быть», и у него два вида: Some(значение) — значение есть,
и None — его нет. Option<u32> читается как «число u32 или
ничего».
Так best_id объявляется теперь, и так в него кладут найденную цель:
// Лучшей цели пока нет — None, то есть «ничего».let mut best_id: Option<u32> = None;
// Нашли цель получше — кладём её ID внутрь Some.best_id = Some(id);Две переменные превратились в одну: «нет цели» и «есть цель с таким-то
ID» теперь записаны в самом best_id.
Достаём значение: if let
Заголовок раздела «Достаём значение: if let»Достать число из Option прямо так нельзя, Rust сначала просит
проверить, лежит ли там что-нибудь. Для этого есть if let:
// Если в best_id лежит Some, называем то, что внутри, id.if let Some(id) = best_id { // Достаём координаты выбранной цели по её ID. let (target_x, target_y) = targets[&id]; look_x = target_x; look_y = target_y;}Если в best_id лежит None, блок просто пропускается, и до
targets[&id] дело не доходит. Забыть проверку теперь не получится:
пока не проверил, числа у тебя нет.
К if let можно добавить else. Он сработает, когда внутри None. Так
мы и сделаем для клика: есть цель — убираем её, нет — расставляем поле
заново.
get: достать по ID безопасно
Заголовок раздела «get: достать по ID безопасно»Есть ещё одно место, где Option пригодится. Квадратные скобки
targets[&id] падают, если мишени с таким ID нет. Для безопасного
обращения у HashMap есть метод .get: он ищет значение по ключу и
возвращает Option. Нашёл — Some со значением внутри, не нашёл —
None:
if let Some(id) = best_id { // get ищет мишень по ID: Some с координатами, если она есть, иначе None. if let Some(&(target_x, target_y)) = targets.get(&id) { look_x = target_x; look_y = target_y; }}.get отдаёт не сами координаты, а ссылку на них, поэтому в образце
стоит &(target_x, target_y), как в
for (&id, &(x, y)) in &targets. А &id в targets.get(&id) — по той
же причине, что и в квадратных скобках: HashMap нужно только
посмотреть на ключ.
Второй if let может показаться лишним: ID мы взяли из самой targets,
значит, мишень есть. Но Rust этого не знает, и .get всё равно
возвращает Option. Зато программа здесь не упадёт ни при каких
обстоятельствах.
Сравнение
Заголовок раздела «Сравнение»Option можно сравнивать, как обычное значение. Чтобы покрасить
выбранную мишень, пишем best_id == Some(id): «лучшая цель — это
именно эта».
Замени содержимое src/main.rs на это:
use macroquad::prelude::*;use std::collections::HashMap;
// Радиус мишени.const TARGET_RADIUS: f32 = 40.0;// Сколько мишеней расставляем.const TARGET_COUNT: u32 = 10;
// Своя функция: по двум точкам считает направление от первой ко второй,// как в уроке «Свои функции».fn direction(from_x: f32, from_y: f32, to_x: f32, to_y: f32) -> (f32, f32) { let dx = to_x - from_x; let dy = to_y - from_y; let distance = (dx * dx + dy * dy).sqrt();
if distance > 0.0 { (dx / distance, dy / distance) } else { (1.0, 0.0) }}
// Своя функция: расстояние между двумя точками, как в уроке «Расстояние».fn distance(from_x: f32, from_y: f32, to_x: f32, to_y: f32) -> f32 { let dx = to_x - from_x; let dy = to_y - from_y; (dx * dx + dy * dy).sqrt()}
// Своя функция: расставляет мишени в случайных местах экрана. Ключ — ID// мишени, значение — её координаты.fn make_targets() -> HashMap<u32, (f32, f32)> { // Мишеней пока нет. let mut targets: HashMap<u32, (f32, f32)> = HashMap::new();
for id in 0..TARGET_COUNT { // Случайная точка, отступив от краёв на радиус, чтобы мишень // целиком поместилась на экране. let x = rand::gen_range(TARGET_RADIUS, screen_width() - TARGET_RADIUS); let y = rand::gen_range(TARGET_RADIUS, screen_height() - TARGET_RADIUS); // Кладём мишень в HashMap под её ID. targets.insert(id, (x, y)); }
targets}
#[macroquad::main("Option")]async fn main() { // Зерно — текущее время, поэтому при каждом запуске поле новое. rand::srand(macroquad::miniquad::date::now() as u64);
// Расставляем мишени при старте. let mut targets = make_targets();
loop { clear_background(BLACK);
// Турель стоит в центре экрана. let center_x = screen_width() / 2.0; let center_y = screen_height() / 2.0;
// Ищем ближайшую цель. ID лучшей цели на данный момент лежит в // Option: пока цель не найдена, там None — «ничего». let mut best_id: Option<u32> = None; // Расстояние до неё. Сначала — самое большое число f32, чтобы // первая же мишень оказалась ближе. let mut best_distance = f32::MAX;
// Смотрим на каждую мишень по очереди. for (&id, &(x, y)) in &targets { // Расстояние от турели до этой мишени. let d = distance(center_x, center_y, x, y); // Строго ближе, чем всё, что видели раньше, — запоминаем её. if d < best_distance { best_distance = d; // Нашли цель получше — кладём её ID внутрь Some. best_id = Some(id); } }
// Точка, на которую смотрит турель. Если целей нет — точка // правее турели. let mut look_x = center_x + 100.0; let mut look_y = center_y; // Если в best_id лежит Some, называем то, что внутри, id. if let Some(id) = best_id { // get ищет мишень по ID и возвращает Option: Some с // координатами, если такая мишень есть, и None, если нет. if let Some(&(target_x, target_y)) = targets.get(&id) { look_x = target_x; look_y = target_y; } }
// Направление от турели к этой точке. let (direction_x, direction_y) = direction(center_x, center_y, look_x, look_y);
// Глазик — центр турели плюс направление, растянутое на 25 пикселей. let eye_x = center_x + direction_x * 25.0; let eye_y = center_y + direction_y * 25.0;
// Кнопка мыши только что нажата. if is_mouse_button_pressed(MouseButton::Left) { if let Some(id) = best_id { // Убираем выбранную цель. Читать targets мы уже закончили. targets.remove(&id); } else { // В best_id None, целей не осталось — расставляем поле заново. targets = make_targets(); } }
// Рисуем мишени. for (&id, &(x, y)) in &targets { // Сначала красим мишень в красный. let mut color = RED; // Если выбранная цель — это именно эта, красим в оранжевый. if best_id == Some(id) { color = ORANGE; } draw_circle(x, y, TARGET_RADIUS, color); }
// Тело турели. draw_circle(center_x, center_y, 40.0, BLUE); // Глазик — смотрит на выбранную цель. draw_circle(eye_x, eye_y, 10.0, WHITE);
next_frame().await; }}Запусти как обычно:
cargo runКартинка та же, что в прошлом уроке: оранжевая мишень — та, на которую смотрит турель, клик убирает её. Поменялось только то, как записан код. Попробуй сам:
Что здесь происходит
Заголовок раздела «Что здесь происходит»Option<u32>— «число или ничего».Some(5)значит, что число есть, аNoneзначит, что его нет.let mut best_id: Option<u32> = None;заменяет сразу две переменные из прошлого урока: флагfoundи числоbest_id = 0, которое ничего не значило.best_id = Some(id);кладёт ID найденной цели внутрьSome.targets.get(&id)ищет мишень по ID и возвращаетOption:Someс координатами, если она есть, иNone, если нет. В отличие отtargets[&id], она не роняет программу.if let Some(id) = best_id { ... }выполняет блок, только если внутриSome, и называет то, что внутри,id. Пока цели нет, блок пропускается.if let Some(id) = best_id { ... } else { ... }для клика: приSomeубираем цель, приNoneрасставляем поле заново.best_id == Some(id)— сравнениеOptionс обычным значением: «лучшая цель — это именно эта мишень».