【问题标题】:GetHashCode override of object containing generic array包含通用数组的对象的 GetHashCode 覆盖
【发布时间】:2009-03-12 14:08:06
【问题描述】:

我有一个包含以下两个属性的类:

public int Id      { get; private set; }
public T[] Values  { get; private set; }

我已经做到了IEquatable<T> 并像这样覆盖了object.Equals

public override bool Equals(object obj)
{
    return Equals(obj as SimpleTableRow<T>);
}

public bool Equals(SimpleTableRow<T> other)
{
    // Check for null
    if(ReferenceEquals(other, null))
        return false;

    // Check for same reference
    if(ReferenceEquals(this, other))
        return true;

    // Check for same Id and same Values
    return Id == other.Id && Values.SequenceEqual(other.Values);
}

当覆盖object.Equals 时,我当然也必须覆盖GetHashCode。但是我应该实现什么代码?如何从通用数组中创建哈希码?以及如何将它与Id 整数结合起来?

public override int GetHashCode()
{
    return // What?
}

【问题讨论】:

    标签: c# arrays generics hashcode


    【解决方案1】:

    由于这个线程中提出的问题,我发布了另一个回复,说明如果你弄错了会发生什么......主要是,你不能使用数组的GetHashCode();正确的行为是运行时不会打印任何警告...切换 cmets 以修复它:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    static class Program
    {
        static void Main()
        {
            // first and second are logically equivalent
            SimpleTableRow<int> first = new SimpleTableRow<int>(1, 2, 3, 4, 5, 6),
                second = new SimpleTableRow<int>(1, 2, 3, 4, 5, 6);
    
            if (first.Equals(second) && first.GetHashCode() != second.GetHashCode())
            { // proven Equals, but GetHashCode() disagrees
                Console.WriteLine("We have a problem");
            }
            HashSet<SimpleTableRow<int>> set = new HashSet<SimpleTableRow<int>>();
            set.Add(first);
            set.Add(second);
            // which confuses anything that uses hash algorithms
            if (set.Count != 1) Console.WriteLine("Yup, very bad indeed");
        }
    }
    class SimpleTableRow<T> : IEquatable<SimpleTableRow<T>>
    {
    
        public SimpleTableRow(int id, params T[] values) {
            this.Id = id;
            this.Values = values;
        }
        public int Id { get; private set; }
        public T[] Values { get; private set; }
    
        public override int GetHashCode() // wrong
        {
            return Id.GetHashCode() ^ Values.GetHashCode();
        }
        /*
        public override int GetHashCode() // right
        {
            int hash = Id;
            if (Values != null)
            {
                hash = (hash * 17) + Values.Length;
                foreach (T t in Values)
                {
                    hash *= 17;
                    if (t != null) hash = hash + t.GetHashCode();
                }
            }
            return hash;
        }
        */
        public override bool Equals(object obj)
        {
            return Equals(obj as SimpleTableRow<T>);
        }
        public bool Equals(SimpleTableRow<T> other)
        {
            // Check for null
            if (ReferenceEquals(other, null))
                return false;
    
            // Check for same reference
            if (ReferenceEquals(this, other))
                return true;
    
            // Check for same Id and same Values
            return Id == other.Id && Values.SequenceEqual(other.Values);
        }
    }
    

    【讨论】:

    • 您能解释一下正确版本的 GetHashCode() 背后的原因吗?
    • @Vinko:你能澄清一下吗?你的意思是“为什么哈希码很重要?” - 或“为什么采用这种方法?”。鉴于您的代表和答案数量,我假设后者;这只是一种获取将所有值考虑在内的散列的方法“乘以素数并添加下一个散列”是一种非常常见的散列方法,可以避免冲突(对比异或;在这种情况下,“所有8s" 可以很容易地给出可预测的哈希码 0)。我错过了什么吗?
    • 另见:stackoverflow.com/questions/263400#263416...不同的质数,但效果相同。
    • 是的,这就是问题所在。谢谢。
    • 我重新开始了,抱歉。 stackoverflow.com/questions/2626839/…,无论如何,我的目标是平等,我希望我可以跳过 GetHashCode 实现部分。是的,初始值为 0。无论如何,我使用 EF,所以所有对象都使用 ID 初始化为 0,然后属性被一个一个单独设置,而不是由初始化程序,这就是如果它在 ID 时被散列的原因是'还没有加载它就疯了,也许你会知道如何解决它并享受正确的散列以及这个可变对象上的相等性。
    【解决方案2】:

    FWIW,在哈希码中使用 Values 的内容是非常危险的。只有当你能保证它永远不会改变时,你才应该这样做。但是,由于它是暴露的,我认为不能保证它是可能的。对象的哈希码永远不应该改变。否则,它将失去其作为 Hashtable 或 Dictionary 中的键的值。考虑一下在 Hashtable 中使用对象作为 key 很难发现的 bug,它的 hashcode 由于外部影响而发生变化,您在 Hashtable 中再也找不到它了!

    【讨论】:

    • 这需要更多的支持。我总是在 GetHashCode 的概念和下载文件的“MD5 哈希”之间做出错误的假设。 GetHashCode 不是用来比较内容的,而是用来比较容器的。确保它指向内存中的相同位置。我使用 GetHashCode 来验证对象自上次保存到数据库后是否发生了变化。我保留了一个克隆列表只是为了比较对象,但是在覆盖 GetHashCode 之后,基于哈希表的所有内容都开始表现得很奇怪。现在我只是将我的覆盖移到它自己的方法上,并用“内容哈希”保留一个字典
    • @Pluc:“GetHashCode 旨在确保容器指向内存中的同一位置。”,不完全是。它的意思是比较内容,只是它可能由于碰撞而产生误报。与 MD5 类似,但发生碰撞的可能性更大。
    • its hashcode changes because of an outside influence and you can no longer find it in the Hashtable! - 对我来说很有意义,如果对象被更改,它就不再是同一个对象,因此它不应该在哈希表、字典、哈希集或其他任何东西中。
    • 对,正因为如此,现在框架中有一个System.HashCode 类可用,它允许您以安全的方式组合哈希码:使用.Add 方法添加一个哈希码(您从任何变量的.GetHashCode() 获取)并使用.ToHashCode() 根据添加计算新的哈希码。使用这些方法重写 Marc 的答案,您将再次处于安全状态。感谢您提出疑问!
    【解决方案3】:

    由于 hashCode 有点像存储对象的键(就像在哈希表中一样),所以我只使用 Id.GetHashCode()

    【讨论】:

    • 嗯,这实际上比使用 Values.GetHashCode( ) 更好,因为它保留了与 Equals 的兼容性。
    【解决方案4】:

    怎么样:

        public override int GetHashCode()
        {
            int hash = Id;
            if (Values != null)
            {
                hash = (hash * 17) + Values.Length;
                foreach (T t in Values)
                {
                    hash *= 17;
                    if (t != null) hash = hash + t.GetHashCode();
                }
            }
            return hash;
        }
    

    这应该与SequenceEqual兼容,而不是对数组做参考比较。

    【讨论】:

    • 比较 Values 的内容是很危险的,因为不能保证它们在对象的生命周期内是相同的。因为数组是暴露的,任何外部类都可以改变它,这会影响hashcode!
    • 重点是它与发布的 Equals 方法兼容。
    • 它也会影响相等性。而且您不能使用对数组的引用来计算哈希码,因为您最终会得到两个具有不同哈希码的相等对象。
    • @Grzenio - 这是针对我还是达斯汀?正是出于这个原因,我不使用该参考...
    • 很抱歉造成混乱,这是对 Dustin 在这里的评论和他的代码同时进行的回复。
    【解决方案5】:

    我只需要添加另一个答案,因为没有提到更明显(且最容易实施)的解决方案之一 - 不包括您的 GetHashCode 计算中的集合!

    这里似乎忘记的主要事情是 GetHashCode 结果的唯一性不是必需的(或者在许多情况下甚至是可能的)。不相等的对象不必返回不相等的哈希码,唯一的要求是相等的对象返回相等的哈希码。所以根据这个定义,GetHashCode 的以下实现对所有对象都是正确的(假设有一个正确的Equals 实现):

    public override int GetHashCode() 
    { 
        return 42; 
    } 
    

    当然,这会在哈希表查找中产生最差的性能,O(n) 而不是 O(1),但它在功能上仍然是正确的。

    考虑到这一点,在为碰巧具有任何类型的集合作为其一个或多个成员的对象实现 GetHashCode 时,我的一般建议是简单地忽略它们并仅根据另一个标量计算 GetHashCode成员。这将非常有效,除非您将大量对象放入哈希表中,其中所有标量成员都具有相同的值,从而产生相同的哈希码。

    在计算哈希码时忽略集合成员也可以提高性能,尽管哈希码值的分布减少了。请记住,使用哈希码可以提高哈希表的性能,因为它不需要调用 N 次 Equals,而只需要调用一次 GetHashCode 并快速查找哈希表。如果每个对象都有一个包含 10,000 个项目的内部数组,这些项目都参与哈希码的计算,那么良好分布所获得的任何好处都可能会丢失。 如果生成哈希码的成本要低得多,那么分布式哈希码会更好。

    【讨论】:

    • 哈希码的目的不仅仅是选择一个哈希桶,更普遍的说是快速剔除可以识别为不相等的东西。如果序列是不可变的,则类应仅将其相等概念基于封装序列的概念。假设序列是不可变的,该类可能应该在其计算的哈希码中包含序列项(它应该反过来可能缓存)。否则,如果向字典中添加 10 个对象,其中包含 5,000 项数组,但最后一个元素不同,则尝试查找元素将导致...
    • ...新元素的所有 5,000 个元素与十个对象中的每一个的所有 5,000 个元素进行比较。相比之下,如果每个项目都计算并缓存了数组内容的哈希值,即使所有十个哈希值都映射到同一个哈希桶,如果所有哈希值不同,最可能发生的情况是新对象将与其他十个缓存的哈希值进行比较。如果几个哈希值发生冲突,那仍然不是真正的问题——只是多出一堆 5,000 个元素的比较(而不是十个)。
    • @supercat:你在这里做了很多假设:序列是不可变的,对象缓存了自己的哈希码(我从未见过),但最重要的是对象是唯一的哈希码所基于的数据是序列(请注意,在原始问题中,对象具有Id 属性,几乎在所有情况下都足以生成唯一的哈希码)。无论如何,你说的是一个非常特殊的场景,我看不出它与一般情况或原始问题有何关系。
    • 如果序列不是不可变的,它不应该参与equals。我认为该类型是不可变的假设是基于 OP 想要测试序列是否相等。如果一个人可能拥有许多对象实例并相互比较,这些对象实例除了某些特征外都是相同的(根据equals 使用的定义),那么该特征通常应该是哈希码的一部分。 Java 认为值得为其最常见的“不可变序列”类型(字符串)缓存哈希码。
    • 我不敢相信我正在阅读这篇文章。我专门编写的最后一个 GetHashCode() 必须枚举对象中的一个集合才能工作,Equals() 也是如此。
    【解决方案6】:
    public override int GetHashCode() {
       return Id.GetHashCode() ^ Values.GetHashCode();  
    }
    

    cmets 和其他答案中有几个优点。如果将对象用作字典中的键,则 OP 应考虑是否将值用作“键”的一部分。如果是,那么它们应该是哈希码的一部分,否则,不是。

    另一方面,我不确定为什么 GetHashCode 方法应该反映 SequenceEqual。它的目的是计算哈希表的索引,而不是完全相等的决定因素。如果使用上面的算法有很多哈希表冲突,并且如果它们的值的顺序不同,那么应该选择一个考虑顺序的算法。如果顺序并不重要,请节省时间,不要考虑它。

    【讨论】:

    • 我也不认为数组在考虑所有元素的情况下实现了 GetHashCode
    • 对Values做引用比较,不会和SequenceEqual兼容(即不同的数组内容相同)
    • 伙计们,我之前已经说过了,但是小心使用暴露数组的所有元素。 GetHashCode() 的结果在对象的生命周期内应该是相同的,否则它将不能作为哈希表键。不能保证这个数组不会改变,所以不要在GetHashCode中使用!
    • @Dustin:很好的说明。这就是我说“如果将对象用作键”时的意思。此类对象在充当键时可能不会以会改变其哈希码或相等性的方式发生变化。
    • msdn.microsoft.com/en-us/library/system.object.gethashcode.aspx "如果两个对象比较相等,则每个对象的 GetHashCode 方法必须返回相同的值。" - 其中“比较为相等”表示“Equals()”
    【解决方案7】:

    我会这样做:

    long result = Id.GetHashCode();
    foreach(T val in Values)
        result ^= val.GetHashCode();
    return result;
    

    【讨论】:

    • 相当合理——注意异或会导致很多冲突; 通常首选乘数/加法
    • 有趣,很多人告诉我用 xor 代替。那么我应该阅读更多关于它的内容。
    • 对此作出回应; {3,3,3,3} 的哈希值是多少?和{4,4,4,4}?还是{4,0,0,4}?还是{1,0,1,0}?你看到了问题...
    • @MarcGravell:乘法很差。太糟糕了 C# 没有左或右位滚动。
    • @Backwards_Dave 那将是一个常规的转变。在旋转或循环移位中,移出一侧的位同时移回另一侧。如果连续四次将 0xF9 除以 2,则剩下 0x0F。但是如果将 0xF9 向右旋转 4 个位置(假设是 8 位寄存器),则剩下 0x9F。
    【解决方案8】:

    我知道这个线程已经很老了,但是我写了这个方法来允许我计算多个对象的哈希码。这对这个案例非常有帮助。它并不完美,但它确实满足了我的需求,很可能也满足了你的需求。

    我真的不能为此付出任何代价。我从一些 .net gethashcode 实现中得到了这个概念。我用的是 419(毕竟,它是我最喜欢的大素数),但你可以选择任何合理的素数(不要太小……不要太大)。

    所以,这是我获取哈希码的方法:

    using System.Collections.Generic;
    using System.Linq;
    
    public static class HashCodeCalculator
    {
        public static int CalculateHashCode(params object[] args)
        {
            return args.CalculateHashCode();
        }
    
        public static int CalculateHashCode(this IEnumerable<object> args)
        {
            if (args == null)
                return new object().GetHashCode();
    
            unchecked
            {
                return args.Aggregate(0, (current, next) => (current*419) ^ (next ?? new object()).GetHashCode());
            }
        }
    }
    

    【讨论】:

      【解决方案9】:

      前提是Id和Values永远不会改变,并且Values不为null...

      public override int GetHashCode()
      {
        return Id ^ Values.GetHashCode();
      }
      

      请注意,您的类不是不可变的,因为任何人都可以修改 Values 的内容,因为它是一个数组。鉴于此,我不会尝试使用其内容生成哈希码。

      【讨论】:

      • 会对Values做一个引用比较,不会和SequenceEqual兼容(即不同的数组内容相同)
      • 对,但是因为数组是暴露的,任何外部代码都可以改变它,坦白说比较内容是很危险的。
      • 所以我真的应该只使用 Id 的 HashCode 吗?
      • 也就是说……如果Equals的结果改变了,GetHashCode的结果不一定要改变,但是如果GetHashCode改变了,那么Equals也会改变?
      • 不一定。对 Values 的引用不应该改变(除非你在你的代码中改变它)——所以应该可以使用它。约翰桑德斯在这里有最好的答案。
      猜你喜欢
      • 1970-01-01
      • 2012-07-13
      • 2021-07-16
      • 2017-03-26
      • 2012-05-21
      • 2011-05-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多