Rust中HashSet未遵循自定义Hash实现?求助
问题:HashSet未遵循自定义Hash实现导致重复插入Pile
我正尝试实现一种类似Storage的结构:物品有特定数量,该数量由不同单位的小型pile组成(还可能包含其他元数据,示例中未体现)。要求每个单位(或单位加其他元数据的组合)最多对应一个pile。我尝试使用HashSet存储pile,并为Pile实现自定义Hash,仅纳入需保证唯一性的信息——即忽略quantity字段,本以为能保证唯一性并快速查找pile以修改quantity。但实际未达预期:HashSet似乎未遵循该自定义Hash实现,quantity仍被用于插入和查找判断。请问我哪里出错了?
最小可复现示例(MWE)
use std::collections::HashSet; use std::hash::{Hash, Hasher}; #[derive(Clone, Debug, PartialEq, Eq)] struct Pile { amount: Amount, } #[derive(Clone, Debug, PartialEq, Eq)] struct Amount { unit: Unit, quantity: u64, } #[derive(Clone, Debug, Eq, Hash, PartialEq)] enum Unit { A, // there exist more variants than A but I'm not going to need them for the MWE _B, } impl Hash for Pile { fn hash<H: Hasher>(&self, state: &mut H) { // ignore `amount.quantity` self.amount.unit.hash(state); } } fn main() { // two `Pile`s with equal amount.unit, so I expect them to have equal hashes let pile1 = Pile { amount: Amount { unit: Unit::A, quantity: 1, }, }; let pile2 = Pile { amount: Amount { unit: Unit::A, quantity: 2, }, }; let mut set = HashSet::new(); set.insert(pile1.clone()); assert!(set.contains(&pile1)); // this is as expected // but if both `Pile`s hash to the same value the set SHOULD "contain" pile2, shouldn't it? assert!(!set.contains(&pile2)); // similarly I would expect the following to replace pile1 set.insert(pile2.clone()); // instead the set contains both now assert_eq!(set.len(), 2); // rather than 1 as expected assert!(set.contains(&pile1)); assert!(set.contains(&pile2)); }
我知道可以用HashMap<Unit, i64>实现需求,但因存在需纳入唯一性判断的额外元数据,需定义新结构体组合Unit与元数据,这会将无关数据耦合,且难以对外提供直观的类型引用(如&Amount),故希望避免这种方式。
问题原因
HashSet的元素唯一性判断依赖两个必须一致的条件:
- 两个元素的哈希值相等(由
Hash实现定义) - 两个元素本身相等(由
Eq/PartialEq实现定义)
你只重写了Pile的Hash实现以忽略quantity,但Pile的PartialEq和Eq是自动派生的,会比较所有字段(包括quantity)。因此,即使pile1和pile2哈希值相同,PartialEq会返回false,HashSet会将它们视为不同元素,允许重复插入。
解决方案
让Hash和Eq的实现逻辑保持一致:重写Pile的PartialEq和Eq,仅比较用于唯一性判断的字段(即unit,后续可扩展其他元数据)。
修改后的代码如下:
use std::collections::HashSet; use std::hash::{Hash, Hasher}; #[derive(Clone, Debug)] struct Pile { amount: Amount, } #[derive(Clone, Debug, PartialEq, Eq)] struct Amount { unit: Unit, quantity: u64, } #[derive(Clone, Debug, Eq, Hash, PartialEq)] enum Unit { A, _B, } impl Hash for Pile { fn hash<H: Hasher>(&self, state: &mut H) { self.amount.unit.hash(state); } } // 手动实现PartialEq,仅比较unit字段 impl PartialEq for Pile { fn eq(&self, other: &Self) -> bool { self.amount.unit == other.amount.unit } } // 手动实现Eq(只需声明,无需额外逻辑) impl Eq for Pile {} fn main() { let pile1 = Pile { amount: Amount { unit: Unit::A, quantity: 1, }, }; let pile2 = Pile { amount: Amount { unit: Unit::A, quantity: 2, }, }; let mut set = HashSet::new(); set.insert(pile1.clone()); assert!(set.contains(&pile1)); // 现在会返回true,因为PartialEq只比较unit assert!(set.contains(&pile2)); // 插入pile2会替换pile1 set.insert(pile2.clone()); assert_eq!(set.len(), 1); // 符合预期 // 注意:此时set中存储的是pile2,因为insert会替换已存在的元素 assert!(set.contains(&pile2)); }
注意事项
- 手动实现
PartialEq后,Pile的相等性判断会忽略quantity,在其他业务场景使用时需要确认是否符合预期。 - 如果后续需要加入更多元数据到唯一性判断中,只需同时修改
Hash和PartialEq的实现,保持两者逻辑一致即可。
内容的提问来源于stack exchange,提问作者Abun
相关产品推荐
相关产品推荐

