代码之家  ›  专栏  ›  技术社区  ›  Neo

是否应该为可变类型上的IEquatable实现GetHashCode?

  •  3
  • Neo  · 技术社区  · 6 年前

    我正在实施 IEquatable<T> ,我很难就 GetHashCode 重写可变类。

    以下资源都提供了一个实现,其中 方法 如果对象发生更改,将在对象的生存期内返回不同的值:

    然而 this link 声明: 方法 应该 因为如果对象是集合的一部分,它可能会导致不希望的行为(我也一直这样理解)。

    有趣的是 MSDN example 实现 方法 仅使用符合我理解的不可变属性。但我不明白为什么其他资源没有涵盖这一点。他们完全错了吗?

    如果一个类型根本没有不可变属性,编译器会发出警告 方法 在我重写时丢失 Equals(object) . 在这种情况下,我应该实现它并调用 base.GetHashCode() 或者只是禁用编译器警告,或者我错过了什么 方法 是否应始终重写和实现?事实上,如果建议是 方法 不应该为可变类型实现,为什么还要为不可变类型实现呢?与默认值相比,它只是为了减少碰撞吗 方法 实现,还是它实际上添加了更具体的功能?

    总结一下我的问题,我的困境是 方法 在可变对象上,意味着如果对象上的属性发生更改,它可以在对象的生存期内返回不同的值。但不使用它意味着,比较可能等价的对象的好处将丢失,因为它将始终返回唯一的值,因此集合将始终返回使用 Equals 用于其操作。

    把这个问题打出来后, another Question 出现在“类似问题”框中,似乎涉及相同的主题。答案似乎非常明确,因为在 方法 实施如果没有,那就不要写了。 Dictionary<TKey, TValue> 仍将正常工作,尽管不是O(1)性能。

    3 回复  |  直到 6 年前
        1
  •  1
  •   Gian Paolo    6 年前

    可变类在字典和其他依赖GetHashCode和Equals的类中工作得很糟糕。

    在您描述的场景中,使用可变对象,我建议您执行以下操作之一:

    class ConstantHasCode: IEquatable<ConstantHasCode>
    {
        public int SomeVariable;
        public virtual Equals(ConstantHasCode other)
        {
            return other.SomeVariable == SomeVariable;
        }
    
        public override int GetHashCode()
        {
            return 0;
        }
    }
    

    class ThrowHasCode: IEquatable<ThrowHasCode>
    {
        public int SomeVariable;
        public virtual Equals(ThrowHasCode other)
        {
            return other.SomeVariable == SomeVariable;
        }
    
        public override int GetHashCode()
        {
            throw new ApplicationException("this class does not support GetHashCode and should not be used as a key for a dictionary");
        }
    }
    

    对于第一种情况,Dictionary的工作(几乎)与预期一样,在查找和插入方面会有性能损失:在这两种情况下,将为Dictionary中已经存在的每个元素调用Equals,直到比较返回true为止。实际上,您正在恢复到列表的性能

    第二种方法是告诉程序员将使用您的类“不,您不能在字典中使用它”。 不幸的是,据我所知,没有在编译时检测它的方法,但当代码第一次向字典中添加元素时,这将失败,很可能是在开发的早期,而不是仅在具有不可预测的输入集的生产环境中发生的那种错误。

    最后但并非最不重要的一点是,忽略“可变”问题,并使用成员变量实现GetHashCode:现在您必须知道,当类与字典一起使用时,您不能随意修改它。在某些情况下,这是可以接受的,但在另一种情况下,这是不可接受的

        2
  •  0
  •   Karolis Kajenas    6 年前

    这完全取决于 collection 你所说的类型。对于我的回答,我假设你在谈论 Hash Table 基于 collections 特别是我将为其解决这一问题。净额 Dictionary Key 计算

    所以,确定如果修改 key (考虑到您的 钥匙 是一个进行自定义的类 HashCode 计算)是为了查看。净来源。从…起我们可以看到您的 key value pair 现在已包装到 Entry 承载的结构 hashcode 计算日期 addition 你的价值。意思是如果你改变 散列值 值添加密钥后,它将无法再在中找到值 dictionary .

    证明它的代码:

        static void Main()
        {
            var myKey = new MyKey { MyBusinessKey = "Ohai" };
            var dic = new Dictionary<MyKey, int>();
            dic.Add(myKey, 1);
            Console.WriteLine(dic[myKey]);
            myKey.MyBusinessKey = "Changing value";
            Console.WriteLine(dic[myKey]); // Key Not Found Exception.
        }
    
        public class MyKey
        {
            public string MyBusinessKey { get; set; }
            public override int GetHashCode()
            {
                return MyBusinessKey.GetHashCode();
            }
        }
    

    .NET source reference.

    所以来回答你的问题。您希望具有作为基础的不可变值 哈希代码 计算时间。

    还有一点, 哈希代码 对于自定义类,如果不重写 GetHashCode 将基于 object . 因此,我们担心返回相同的 哈希代码 对于基础值相同的不同对象,可以通过 overriding GetHashCode 方法和计算 散列值 取决于您的业务密钥。例如,您有两个字符串属性,要计算hashcode,您需要 concat strings 和呼叫基地 string 方法 方法这将保证你得到同样的 哈希代码 对于相同的基础值 对象 .

        3
  •  -3
  •   Neo    6 年前

    经过多次讨论和阅读其他SO关于该主题的答案,最终 this ReSharper help page 这对我来说是一个很好的总结:

    MSDN documentation GetHashCode() 方法不显式要求重写此方法返回一个在对象的生存期内从不更改的值。具体来说,它说:

    对象的GetHashCode方法必须始终返回相同的哈希代码,只要不修改确定对象的Equals方法返回值的对象状态。

    另一方面,它表示哈希代码至少在对象位于集合中时不应更改:

    *可以重写不可变引用类型的GetHashCode。通常,对于可变引用类型,只有在以下情况下才应重写GetHashCode:

    • 您可以从不可变的字段计算哈希代码;或
    • 可以确保可变对象包含在依赖其哈希代码的集合中时,其哈希代码不会更改*

    但你为什么要重写 GetHashCode() 首先?通常,如果您的对象将用于 哈希表 ,如字典中的键等,很难预测对象何时添加到集合中,以及在集合中保留多长时间。

    综上所述,如果你想安全起见,请确保 GetHashCode() 在对象的生存期内返回相同的值。ReSharper将通过指向实现中的每个非只读字段或非get-only属性来帮助您 GetHashCode() . 如果可能,ReSharper还将建议 quick-fixes 使这些成员为只读/只读。

    当然,如果不可能快速修复,它不会建议该怎么做。然而,它确实表明,只有“在可能的情况下”才应使用这些快速修复方法,这意味着可能会抑制检查。Gian Paolo对此的回答建议抛出一个异常,该异常将阻止类被用作键,并且如果无意中将其用作键,则会在开发早期出现。

    然而 GetHashCode 用于其他情况,例如将对象的实例作为参数传递给模拟方法设置时。因此,唯一可行的选择是实施 方法 使用可变值并让代码的其余部分承担责任,以确保对象在用作键时不会发生变化,或者根本不将其用作键。