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

Уроки · 45

Кружки исчезают

До сих пор Vec только рос — кружки добавлялись по одному и оставались в списке навсегда. А что, если кружок нужно убрать?

У for уже было два знакомых вида: &circles — одолжить список только для чтения, и &mut circles — одолжить для изменения на месте. У обоих есть общая черта: сам список, сколько бы элементов в нём ни было, остаётся тем же самым списком до и после цикла — их количество через такой for не поменять.

Чтобы выбросить элемент, нужен третий вид — for circle in circles, вовсе без &. Он не одалживает список, а забирает его в собственность целиком: каждый элемент по очереди становится собственностью переменной circle внутри цикла, а исходная переменная circles после такого цикла больше не существует — воспользоваться ей ещё раз нельзя.

Можно спросить: а почему бы просто не прочитать список через &circles и не скопировать нужные кружки в новый, без всякого «отбирания»? Технически можно — но это значит по-настоящему дублировать данные каждого оставшегося кружка: создавать второй, независимый экземпляр только затем, чтобы сразу выбросить первый. Владение решает это без копирования: kept_circles.push(circle) ничего не дублирует, а просто переносит те же самые данные в новое место — дешевле, и работает даже для значений, которые копировать вообще не умеют.

План такой: забираем старый список целиком, для каждого кружка решаем, оставить его или нет, и если да — добавляем в новый, изначально пустой список. Кружки, которые не добавили никуда, просто забываются вместе со старым списком.

// Новый пустой список — специально для кружков, которые стоит оставить.
let mut kept_circles: Vec<Circle> = Vec::new();
// Забираем старый список circles в собственность — второй раз им
// воспользоваться уже не выйдет.
for circle in circles {
// Клик попал в этот кружок — не переносим его в новый список.
let clicked_inside = distance(mouse_x, mouse_y, circle.x, circle.y) < circle.radius;
if !clicked_inside {
kept_circles.push(circle);
}
}
// На следующий кадр circles станет тем, что мы только что собрали.
circles = kept_circles;

distance(...) < circle.radius — та же проверка «мышь внутри круга», что и в уроке «Круглые кнопки». Кружок, в который попал клик, просто не переносится в kept_circles — и пропадает вместе со старым списком, когда цикл закончится.

Замени содержимое src/main.rs на это:

src/main.rs
use macroquad::prelude::*;
// Фиксированный список цветов, из которого выбираем случайный.
const COLORS: [Color; 4] = [YELLOW, RED, GREEN, BLUE];
// Один кружок: положение, радиус и цвет вместе, под понятными именами.
struct Circle {
x: f32,
y: f32,
radius: f32,
color: Color,
}
// Своя функция: расстояние между двумя точками, как в уроке «Расстояние».
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()
}
#[macroquad::main("Кружки исчезают")]
async fn main() {
// Начальный набор кружков — случайные позиции, радиусы и цвета.
let mut circles: Vec<Circle> = Vec::new();
// Номер шага нам не нужен, поэтому вместо имени стоит _.
for _ in 0..10 {
circles.push(Circle {
x: rand::gen_range(40.0, screen_width() - 40.0),
y: rand::gen_range(40.0, screen_height() - 40.0),
radius: rand::gen_range(50.0, 100.0),
color: COLORS[rand::gen_range(0, COLORS.len())],
});
}
loop {
clear_background(BLACK);
// Клик только что случился — убираем кружки под курсором.
if is_mouse_button_pressed(MouseButton::Left) {
let (mouse_x, mouse_y) = mouse_position();
// Новый пустой список — специально для кружков, которые стоит оставить.
let mut kept_circles: Vec<Circle> = Vec::new();
// Забираем старый список circles в собственность.
for circle in circles {
let clicked_inside = distance(mouse_x, mouse_y, circle.x, circle.y) < circle.radius;
if !clicked_inside {
kept_circles.push(circle);
}
}
// На этот кадр circles становится тем, что мы только что собрали.
circles = kept_circles;
}
// Рисуем всё, что осталось.
for circle in &circles {
draw_circle(circle.x, circle.y, circle.radius, circle.color);
}
next_frame().await;
}
}

Запусти как обычно:

Окно терминала
cargo run

На экране — десять кружков в случайных местах. Кликни по любому — исчезнет он сам, а заодно и все остальные, которые тоже оказались под курсором. Попробуй прямо здесь:

  • for _ in 0..10 — тот же for, что и в уроке «Цикл for», только вместо имени счётчика стоит _. Здесь нам нужно просто повторить цикл десять раз, а само число на каждом шаге не используется. Убрать место для имени в for нельзя, оно должно быть. А если написать туда обычное имя и не пользоваться им, компилятор предупредит, что переменная не используется. Поэтому пишем _: это значит «здесь что-то стоит, но оно нам не нужно».
  • for circle in circles без & забирает список в собственность: старая переменная circles после цикла недоступна, поэтому в конце ей присваивается новое значение — kept_circles.
  • Владение здесь выгоднее копирования: kept_circles.push(circle) просто переносит уже готовые данные кружка в новый список, не создавая их дубликат только для того, чтобы сразу выбросить оригинал.
  • Кружок добавляется в kept_circles, только если клик не попал внутрь него — те, что попали, никуда не переносятся и пропадают вместе со старым списком.
  • !clicked_inside — отрицание через !, та же идея, что и в уроке «Порядок отрисовки»: переворачивает bool на противоположный.
  • Если клик не попал ни в один кружок, kept_circles получится точно такой же, как старый circles — ни один кружок не потеряется.