Уроки · 69
HashMap
В уроке «Что такое идентификатор» выяснили: чтобы ID
не повторялись, это должен поддерживать сам код — Rust за этим не
следит. Но есть структура, которая как раз следит: HashMap. Ключ в
ней гарантированно не может повториться — это свойство самой
структуры, а не то, что нужно обеспечивать руками.
HashMap — пары «ключ → значение»
Заголовок раздела «HashMap — пары «ключ → значение»»HashMap хранит пары: у каждого значения есть свой ключ, и по ключу
это значение можно сразу найти. Для наших кружков ключом будет ID, а
значением — координаты:
use std::collections::HashMap;
let mut circles: HashMap<u32, (f32, f32)> = HashMap::new();В угловых скобках у HashMap не один тип, а два, через запятую — та
же идея, что и у Vec<T> из урока «Список кружков», просто
здесь нужно указать сразу оба: тип ключа и тип значения.
HashMap<u32, (f32, f32)> значит «ключ — число u32, значение — пара
чисел».
ID больше не нужно хранить внутри кортежа отдельным числом, как в
уроке «Идентификатор кружка» — он и так ключ, HashMap
уже держит его рядом с координатами.
Добавить пару в HashMap — метод .insert. Сам ряд считается той же
формулой START_X + STEP * i, что и в уроке «Цикл for»:
for i in 0..COUNT { circles.insert(i as u32, (START_X + STEP * (i as f32), screen_height() / 2.0));}Удаление без пересборки списка
Заголовок раздела «Удаление без пересборки списка»В уроках «Кружки исчезают» и «Ловушка
индекса» убрать один кружок означало пересобрать
весь список заново, без него. С HashMap это один вызов:
circles.remove(&clicked_id);Но перед этим нужно узнать, какой именно id под курсором — а
перебирать circles и одновременно удалять из неё нельзя: пока идёт
чтение (for ... in &circles), HashMap занята чтением и не даст
себя в этот же момент изменить. Поэтому сначала только читаем и, если
клик попал в кружок, запоминаем его id в переменной — а .remove
вызываем уже после цикла, когда чтение закончилось:
let mut found = false;let mut clicked_id: u32 = 0;
for (&id, &(x, y)) in &circles { if distance(mouse_x, mouse_y, x, y) < RADIUS { found = true; clicked_id = id; }}
if found { circles.remove(&clicked_id);}&clicked_id — ключ передаётся по ссылке, а не в собственность.
.remove нужно только посмотреть на id и сравнить его с тем, что
хранится внутри HashMap, — забирать его себе незачем. Для u32 это
не так важно: скопировать такое число ничего не стоит. Но тот же метод
работает с ключами любого типа, и среди них есть более тяжёлые, вроде
String — туда отдавать владение ключом только ради проверки на
совпадение было бы расточительно.
Замени содержимое src/main.rs на это:
use macroquad::prelude::*;use std::collections::HashMap;
// Радиус один и тот же у всех кружков — фиксированный, как в уроке «Константы».const RADIUS: f32 = 84.0;
// Сколько кружков рисуем и как их расставить в ряд — те же START_X и STEP,// что и в уроке «Цикл for».const COUNT: usize = 10;const START_X: f32 = 100.0;const STEP: f32 = 228.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, а не номер в списке.const TARGET_ID: u32 = 3;
#[macroquad::main("HashMap")]async fn main() { // Ряд кружков: ключ — ID, значение — координаты. ID не нужно хранить // ещё и внутри значения — он и так ключ. let mut circles: HashMap<u32, (f32, f32)> = HashMap::new(); for i in 0..COUNT { circles.insert(i as u32, (START_X + STEP * (i as f32), screen_height() / 2.0)); }
loop { clear_background(BLACK);
// Клик только что случился — ищем, в какой кружок попали. if is_mouse_button_pressed(MouseButton::Left) { let (mouse_x, mouse_y) = mouse_position();
// Сначала только читаем circles — удалять во время чтения нельзя. let mut found = false; let mut clicked_id: u32 = 0;
for (&id, &(x, y)) in &circles { if distance(mouse_x, mouse_y, x, y) < RADIUS { found = true; clicked_id = id; } }
// Теперь, когда чтение закончилось, можно убрать найденный кружок. if found { circles.remove(&clicked_id); } }
// Рисуем каждый кружок, подписываем его ID и, если это и есть // TARGET_ID, тут же обводим его кольцом. for (&id, &(x, y)) in &circles { draw_circle(x, y, RADIUS, YELLOW); draw_text(&id.to_string(), x, y, 128.0, BLACK);
if id == TARGET_ID { draw_circle_lines(x, y, RADIUS + 10.0, 4.0, WHITE); } }
next_frame().await; }}Запусти как обычно:
cargo runКликай по кружкам — каждый клик убирает один, без всякой пересборки
списка. Порядок, в котором кружки перебираются и рисуются, тут не
гарантирован — HashMap, в отличие от Vec, порядок вставки не
хранит. Для этой задачи это не важно: на какой кружок ни показывай
пальцем, ID у него свой, не зависящий от места в переборе. Попробуй
сам:
Что здесь происходит
Заголовок раздела «Что здесь происходит»HashMap<u32, (f32, f32)>хранит пары «ID → координаты» — ключ (u32) и значение ((f32, f32)) вместе, одним элементом..insert(id, value)добавляет пару,.remove(&id)убирает её по ключу — оба без перебора всего списка.- Перебирать
&circlesи одновременно звать.remove()на ней же нельзя, поэтому сначала собираем нужныйid, а удаляем уже после цикла. for (&id, &(x, y)) in &circles— тот же видfor, что уже встречался уVec, только на каждом шаге вместо одного элемента достаются сразу пара: ключ и значение.
HashMap не следит, в каком порядке лежат пары, зато следит за тем, о
чём шла речь в прошлом уроке — что один и тот же ключ не встретится
дважды. То, что раньше было правилом, которое мы обязаны были
соблюдать сами, здесь стало свойством самой структуры.