Перейти к содержимому

Уроки · 69

HashMap

В уроке «Что такое идентификатор» выяснили: чтобы ID не повторялись, это должен поддерживать сам код — Rust за этим не следит. Но есть структура, которая как раз следит: 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 на это:

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 не следит, в каком порядке лежат пары, зато следит за тем, о чём шла речь в прошлом уроке — что один и тот же ключ не встретится дважды. То, что раньше было правилом, которое мы обязаны были соблюдать сами, здесь стало свойством самой структуры.