Avatar ·

Как итерироваться по HashMap, печатая ключ/значение, и удалять элементы в Rust?

Всем привет! Я новичок в Rust и пытаюсь решить простую задачу: у меня есть `HashMap`, и я хочу пройтись по всем её элементам, вывести на экран пары "ключ-значение", а затем удалить некоторые из них по условию (например, все, где значение меньше 10). Проблема в том, что borrow checker не даёт мне это сделать. Вот мой код, который не компилируется: ```rust use std::collections::HashMap; fn main() { let mut map = HashMap::new(); map.insert("apple".to_string(), 5); map.insert("banana".to_string(), 15); map.insert("cherry".to_string(), 3); for (key, value) in &map { println!("{}: {}", key, value); if *value < 10 { map.remove(key); // Ошибка! } } } ``` Компилятор выдаёт ошибку: `cannot borrow 'map' as mutable because it is also borrowed as immutable`. Я понимаю, что это связано с правилами заимствования, но не знаю, как правильно обойти это ограничение. Пробовал использовать `.iter()` и `.iter_mut()`, но всё равно получаю ошибки. Подскажите, как правильно итерироваться по HashMap и удалять элементы во время итерации? Может быть, есть какой-то паттерн или идиома для этого в Rust? <<>>

Почему возникает ошибка?

В Rust действуют строгие правила заимствования: вы не можете одновременно иметь неизменяемую ссылку на коллекцию (через &map в цикле for) и изменяемую ссылку (через map.remove()). Итератор for (key, value) in &map заимствует map неизменяемо, а удаление требует изменяемого заимствования. Компилятор справедливо блокирует такой код, чтобы предотвратить неопределённое поведение.

Правильное решение: метод drain() или итерация с клонированием ключей

Самый простой и безопасный способ — собрать ключи для удаления в отдельную коллекцию, а затем удалить их после завершения итерации. Вот как это сделать:

use std::collections::HashMap;

fn main() {
    let mut map = HashMap::new();
    map.insert("apple".to_string(), 5);
    map.insert("banana".to_string(), 15);
    map.insert("cherry".to_string(), 3);

    // Шаг 1: собираем ключи для удаления
    let keys_to_remove: Vec<String> = map
        .iter()
        .filter(|(_, &value)| value < 10)
        .map(|(key, _)| key.clone())
        .collect();

    // Шаг 2: удаляем ключи
    for key in keys_to_remove {
        map.remove(&key);
    }

    // Печатаем оставшиеся элементы
    for (key, value) in &map {
        println!("{}: {}", key, value);
    }
}

Этот подход полностью безопасен и понятен. Вы сначала собираете ключи (клонируя их, если они не Copy), а потом удаляете. Итерация и мутация разнесены во времени, поэтому borrow checker доволен.

Более продвинутый метод: retain()

Если ваша цель — удалить элементы по условию, а не просто напечатать их, идеальным решением будет метод retain(). Он принимает замыкание, которое получает ссылку на ключ и значение, и если замыкание возвращает false, элемент удаляется:

use std::collections::HashMap;

fn main() {
    let mut map = HashMap::new();
    map.insert("apple".to_string(), 5);
    map.insert("banana".to_string(), 15);
    map.insert("cherry".to_string(), 3);

    // Сначала печатаем все элементы
    for (key, value) in &map {
        println!("{}: {}", key, value);
    }

    // Удаляем элементы со значением меньше 10
    map.retain(|_, &mut value| value >= 10);

    // Печатаем оставшиеся
    for (key, value) in &map {
        println!("После удаления: {}: {}", key, value);
    }
}

retain() — это самый идиоматичный способ в Rust для фильтрации HashMap на месте. Он безопасен, эффективен и не требует дополнительных выделений памяти.

Когда нужно печатать во время удаления

Если вам обязательно нужно печатать каждый элемент и сразу же его удалять (например, вы обрабатываете очередь), используйте drain():

use std::collections::HashMap;

fn main() {
    let mut map = HashMap::new();
    map.insert("apple".to_string(), 5);
    map.insert("banana".to_string(), 15);
    map.insert("cherry".to_string(), 3);

    // drain() удаляет все элементы и передаёт их в итератор
    for (key, value) in map.drain() {
        println!("Удаляем: {}: {}", key, value);
    }

    println!("Карта пуста: {}", map.is_empty()); // true
}

Но будьте осторожны: drain() удаляет все элементы. Если нужно удалить только часть, комбинируйте с фильтрацией:

// Удаляем только элементы со значением < 10, остальные оставляем
let filtered: HashMap<_, _> = map
    .drain()
    .filter(|(_, value)| *value >= 10)
    .collect();
// Теперь map пуст, все данные в filtered

Итог

  • Простое удаление по условию: используйте retain().
  • Удаление с предварительным сбором ключей: собирайте ключи в Vec, потом удаляйте.
  • Полная очистка с обработкой: используйте drain().
  • Никогда не пытайтесь изменять HashMap во время итерации по ссылке — это запрещено правилами заимствования.
<<>>

Альтернативный подход: итерация с использованием Vec<String> и замыканий

Если вам нужно выполнить более сложные действия при удалении (например, логирование или модификацию значений), можно использовать комбинацию iter() и remove(), но с предварительным сбором ключей. Вот элегантный способ с использованием итераторов:

use std::collections::HashMap;

fn main() {
    let mut map = HashMap::new();
    map.insert("apple".to_string(), 5);
    map.insert("banana".to_string(), 15);
    map.insert("cherry".to_string(), 3);

    // Собираем ключи для удаления (без клонирования, если ключи Copy)
    let keys_to_remove: Vec<_> = map
        .iter()
        .filter_map(|(key, &value)| if value < 10 { Some(key.clone()) } else { None })
        .collect();

    // Удаляем и выводим
    for key in keys_to_remove {
        if let Some(value) = map.remove(&key) {
            println!("Удалён: {}: {}", key, value);
        }
    }

    // Оставшиеся элементы
    println!("Осталось: {:?}", map);
}

Этот метод особенно полезен, когда нужно не только удалить, но и обработать удалённое значение (например, записать в лог или передать в другой поток). Он даёт полный контроль над процессом и при этом остаётся безопасным.

Также обратите внимание: если ваши ключи реализуют трейт Copy (например, i32), клонирование не требуется — можно просто использовать *key.