You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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的元素唯一性判断依赖两个必须一致的条件:

  1. 两个元素的哈希值相等(由Hash实现定义)
  2. 两个元素本身相等(由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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 20:04:59