PAYATHON 2026

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,不要依赖示例程序当前的打印顺序。

备注:内容仅供参考。