Как итерироваться по 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.