Управление временем жизни сборщика мусора в Rust: как правильно освобождать память?
Проблема: Rust не имеет встроенного GC, но это не делает его сложнее
Вы столкнулись с фундаментальным отличием Rust от Java. В Rust нет сборщика мусора, но это не проблема — он использует систему владения (ownership) и заимствования (borrowing), которая гарантирует безопасность памяти на этапе компиляции. Для вашей задачи (создание и удаление объектов в рантайме) отлично подходит вектор (Vec) с методами push и retain. Ваш код на самом деле почти правильный — просто нужно добавить несколько деталей.
Решение: правильное управление временем жизни через Vec и фильтрацию
Вот исправленная версия вашего кода:
struct Bullet {
x: f32,
y: f32,
alive: bool,
}
struct Game {
bullets: Vec<Bullet>,
}
impl Game {
fn new() -> Self {
Game { bullets: Vec::new() }
}
fn spawn_bullet(&mut self, x: f32, y: f32) {
let bullet = Bullet { x, y, alive: true };
self.bullets.push(bullet); // Всё ок: мы перемещаем bullet в вектор
}
fn update(&mut self) {
// Удаляем мёртвые пули
self.bullets.retain(|b| b.alive);
}
}
fn main() {
let mut game = Game::new();
game.spawn_bullet(10.0, 20.0);
game.spawn_bullet(30.0, 40.0);
println!("Количество пуль: {}", game.bullets.len());
// Симулируем смерть первой пули
game.bullets[0].alive = false;
game.update();
println!("После обновления: {}", game.bullets.len()); // Выведет: 1
}
Почему это работает?
Vecвладеет всеми элементами — не нужно думать о ссылках.- Метод
pushперемещает владениеbulletв вектор. Rust сам позаботится об освобождении памяти, когда вектор будет удалён или когда вы вызоветеretain. - Метод
retainудаляет элементы, не прошедшие проверку — это безопасно и эффективно.
Когда использовать Box, Rc или другие умные указатели?
Для вашей задачи Vec — идеальное решение. Другие инструменты нужны в специфических случаях:
Box<T>— когда размер типа неизвестен на этапе компиляции (например, рекурсивные структуры).Rc<T>— когда нужно разделять владение между несколькими частями кода (например, графы).Arc<T>— то же, чтоRc, но для многопоточности.
Как избежать утечек памяти?
В Rust утечки памяти возможны только в двух случаях:
- Вы используете
Box::leakилиstd::mem::forget(специальные функции). - Вы создаёте циклические ссылки с
Rc(нужно использоватьWeak).
В вашем коде с Vec утечек нет — Rust автоматически вызовет деструктор для каждого элемента при удалении вектора.
Производительность: Vec vs GC
Метод retain работает за O(n) и не создаёт пауз, как GC в Java. Если вам нужно часто удалять много элементов, рассмотрите использование Vec::drain или замену мёртвых объектов на "свободные слоты" (free list). Но для 1000 пуль в секунду Vec справится отлично.
В итоге: не пытайтесь имитировать GC в Rust. Используйте Vec для хранения объектов и фильтруйте их через retain или drain. Это быстрее, безопаснее и понятнее, чем сборка мусора.
Альтернативный подход: использование пула объектов (object pool)
Если вы создаёте и уничтожаете объекты очень часто (например, 10 000 частиц в секунду), постоянное выделение и освобождение памяти через Vec::push и retain может стать узким местом. В таких случаях эффективнее использовать пул объектов.
struct BulletPool {
pool: Vec<Bullet>,
active: Vec<usize>, // Индексы активных пуль
}
impl BulletPool {
fn spawn(&mut self, x: f32, y: f32) -> Option<&mut Bullet> {
if let Some(idx) = self.pool.iter().position(|b| !b.alive) {
// Переиспользуем мёртвую пулю
self.pool[idx] = Bullet { x, y, alive: true };
self.active.push(idx);
Some(&mut self.pool[idx])
} else {
// Создаём новую
self.pool.push(Bullet { x, y, alive: true });
let idx = self.pool.len() - 1;
self.active.push(idx);
Some(&mut self.pool[idx])
}
}
fn update(&mut self) {
self.active.retain(|&idx| self.pool[idx].alive);
}
}
Преимущества пула:
- Минимизирует выделение памяти — переиспользует освобождённые слоты.
- Подходит для high-frequency обновлений (частицы, пули в шутерах).
- Легко реализовать с помощью
Vecи дополнительного вектора индексов.
Недостаток: код сложнее, чем просто retain. Используйте пул только если профилировщик показывает, что Vec::retain является узким местом. Для большинства игр с 1000–5000 объектов обычный Vec работает отлично.