Rust HashMap 修改与删除时如何避免 Multiple Borrows Error
结论
这个场景适合使用 HashMap::retain。遍历时,可以通过 &mut V 修改 value,再根据闭包的返回值决定是否保留当前 entry。删除由 HashMap 内部完成,不会产生重叠借用。
use std::collections::HashMap;
pub struct Custom {
x: u8,
}
fn main() {
let mut custom_map: HashMap<String, Custom> = HashMap::new();
custom_map.insert("apple".to_string(), Custom { x: 10 });
custom_map.insert("banana".to_string(), Custom { x: 20 });
custom_map.insert("grape".to_string(), Custom { x: 30 });
custom_map.insert("quince".to_string(), Custom { x: 40 });
custom_map.retain(|key, custom| {
if key.contains('a') {
custom.x += 1;
}
// 返回 false 时删除当前 entry
!key.contains('q')
});
for (key, custom) in &custom_map {
println!("Key: {}, x: {}", key, custom.x);
}
}
retain 的闭包参数分别是 &K 和 &mut V。返回 true 会保留 entry,返回 false 则会删除 entry。因此,它很适合用来完成“检查 key、修改 value、决定是否删除”这类操作。
为什么原代码会一直保持借用
两个操作是否在同一个作用域里,并不能直接决定借用何时结束。Rust 会根据引用最后一次被使用的位置判断借用的有效范围。
原代码中:
let mut elements_to_be_deleted: Vec<&String> = Vec::new();
for (key, custom) in &mut custom_map {
elements_to_be_deleted.push(key);
}
key 引用了 custom_map 内部的 key。将它存入 Vec<&String> 后,引用在循环结束时依然存在,因为后面的删除循环还要使用它:
for key in &elements_to_be_deleted {
custom_map.remove(*key);
}
第一次遍历产生的借用会一直持续到 elements_to_be_deleted 最后一次使用。此时调用 remove,又需要可变借用 custom_map,两次借用就发生了冲突。
这个限制可以防止内部引用失效。HashMap::remove 会改变容器结构,可能导致此前取得的内部引用不再有效。因此,Rust 不允许程序保留指向容器内部的引用,同时对容器进行结构性修改。
相关的借用规则包括:
- 多个不可变借用可以同时存在;
- 存在可变借用时,不能再出现与其重叠的其他借用;
- 向
HashMap插入或删除 entry 属于结构性修改,需要可变借用; - 引用的生命周期通常持续到最后一次使用,不完全由花括号决定。
为什么增加代码块没有效果
即使把遍历放进单独的代码块:
{
for key in custom_map.keys() {
elements_to_be_deleted.push(key);
}
}
elements_to_be_deleted 仍保存着来自 custom_map 的引用。引用已经离开了这个代码块,借用自然不能在右花括号处结束。
拆分函数也不会改变这种关系。下面的类型明确表示容器中保存的是引用:
Vec<&String>
只要后续还要把这些引用传给删除函数,custom_map 就会继续处于被借用状态。拆分函数可以让代码更容易组织,但不会改变数据之间的生命周期关系。
需要分阶段处理时,保存拥有所有权的 key
如果处理逻辑比较复杂,不方便写在 retain 中,可以分两个阶段完成:先遍历并修改 value,同时复制需要删除的 key;等第一次借用结束后,再执行删除。
use std::collections::HashMap;
pub struct Custom {
x: u8,
}
fn main() {
let mut custom_map: HashMap<String, Custom> = HashMap::new();
custom_map.insert("apple".to_string(), Custom { x: 10 });
custom_map.insert("banana".to_string(), Custom { x: 20 });
custom_map.insert("grape".to_string(), Custom { x: 30 });
custom_map.insert("quince".to_string(), Custom { x: 40 });
let mut keys_to_delete: Vec<String> = Vec::new();
for (key, custom) in &mut custom_map {
if key.contains('a') {
custom.x += 1;
}
if key.contains('q') {
keys_to_delete.push(key.to_string());
}
}
for key in &keys_to_delete {
custom_map.remove(key);
}
for (key, custom) in &custom_map {
println!("Key: {}, x: {}", key, custom.x);
}
}
这里将:
Vec<&String>
改成了:
Vec<String>
String 拥有自己的数据,不再引用 custom_map 内部的 key。第一个循环结束后,对 map 的可变借用也随之结束,后续调用 remove 就不会发生冲突。
这种写法需要复制待删除的 key。如果 key 很多,或者复制成本较高,可以优先考虑 retain。
如何选择设计方式
如果一次检查就能完成 value 修改和 entry 去留判断,可以使用 retain:
map.retain(|key, value| {
// 修改 value
// 返回是否保留 entry
true
});
如果删除条件需要多个处理阶段,或者删除前还要完成其他计算,可以先收集拥有所有权的数据,例如 String、ID 或其他可复制的标识,再修改 map。
计划修改容器结构时,不要长期保存指向容器内部的引用。如果 Vec<&K> 后面还会出现 insert、remove 或 clear 等操作,应检查能否把引用改为拥有所有权的值。
HashMap 不保证遍历顺序。需要固定输出顺序时,应先收集并排序 key,不要依赖示例程序当前的打印顺序。
备注:内容仅供参考。
