是否应该在可变类型上为 IEquatable<T> 实现 GetHashCode?

Should GetHashCode be implemented for IEquatable<T> on mutable types?

我正在实施 IEquatable<T>,但我很难就可变 class.

上的 GetHashCode 覆盖达成共识

以下资源都提供了一个实现,如果对象发生变化,GetHashCode 将在对象的生命周期内 return 不同的值:

但是,this link 声明 GetHashCode 不应该 为可变类型实现,因为如果对象是一部分,它可能会导致不良行为一个集合(这也是我一直以来的理解)。

有趣的是,MSDN example 仅使用不可变属性实现了 GetHashCode,这符合我的理解。但我很困惑为什么其他资源没有涵盖这一点。他们只是错了吗?

如果一个类型根本没有不可变的属性,编译器会在我覆盖 Equals(object) 时警告 GetHashCode 丢失。在这种情况下,我应该实现它并只调用 base.GetHashCode() 还是只禁用编译器警告,或者我错过了什么并且 GetHashCode 应该总是被覆盖和实现?事实上,如果建议 GetHashCode 不应该为可变类型实现,为什么还要为不可变类型实现呢?与默认的 GetHashCode 实现相比,它只是为了减少冲突,还是实际上增加了更多有形的功能?

总结一下我的问题,我的困境是,在可变对象上使用 GetHashCode 意味着如果对象的属性发生变化,它可以 return 在对象的生命周期内使用不同的值。但不使用它意味着比较可能等效的对象的好处会丢失,因为它总是 return 一个唯一值,因此集合将始终回退到使用 Equals 进行操作。

输入这个问题后,another Question 出现在 'Similar Questions' 框中,似乎是针对同一主题。那里的答案似乎非常明确,因为在 GetHashCode 实现中只应使用不可变属性。如果有none,那就干脆不写了。 Dictionary<TKey, TValue> 仍然可以正常运行,尽管不是 O(1) 性能。

这完全取决于您所谈论的 collection 类型。对于我的回答,我假设您正在谈论基于 Hash Tablecollections,特别是我将针对 .NET DictionaryKey 计算解决它。

因此,如果您修改 key(假设您的 key 是一个进行自定义 HashCode 计算的 class,那么确定会发生什么情况的最佳方法是查看.NET 源代码。从 .NET 源代码中,我们可以看到您的 key value pair 现在被包装到 Entry 结构中,该结构携带 hashcode,这是根据您的值的 addition 计算得出的。这意味着如果您在添加密钥之后更改 HashCode 值,它将无法再在 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.

所以回答你的问题。您希望拥有不可变的值,作为您的 hashcode 计算的基础。

还有一点,如果不覆盖GetHashCode,自定义class的hashcode将基于object的引用。因此,可以通过 overriding GetHashCode 方法并根据您的业务键计算 HashCode 来减轻对基础值相同的不同对象返回相同 hashcode 的担忧。例如,您将有两个字符串属性,要计算哈希码,您将 concat strings 并调用基础 string GetHashCode 方法。这将保证对于 object.

的相同基础值,您将获得相同的 hashcode

Mutable classes 在 Dictionaries 和其他依赖 GetHashCode 和 Equals 的 classes 上工作非常糟糕。

在您描述的场景中,对于可变对象,我建议采用以下方法之一:

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(几乎)按预期工作,但查找和插入的性能会下降:在这两种情况下,将为字典中已有的每个元素调用 Equals,直到比较 return 为真。您实际上正在恢复 List

的性能

第二种是告诉程序员将使用您的 class "no, you cannot use this within a dictionary" 的方法。 不幸的是,据我所知,没有方法可以在编译时检测到它,但这会在代码第一次向字典添加元素时失败,很可能在开发时很早,而不是仅在生产中发生的那种错误具有一组不可预测的输入的环境。

最后但同样重要的是,忽略 "mutable" 问题并使用成员变量实现 GetHashCode:现在您必须意识到,当 class 与字典一起使用时,您不能随意修改它.在某些情况下这是可以接受的,在其他情况下则不是

经过多次讨论并阅读了有关该主题的其他 SO 答案,最终 this ReSharper help page 对我进行了很好的总结:

MSDN documentation of the GetHashCode() method does not explicitly require that your override of this method returns a value that never changes during the object's lifetime. Specifically, it says:

The GetHashCode method for an object must consistently return the same hash code as long as there is no modification to the object state that determines the return value of the object's Equals method.

On the other hand, it says that the hash code should not change at least when your object is in a collection:

*You can override GetHashCode for immutable reference types. In general, for mutable reference types, you should override GetHashCode only if:

  • You can compute the hash code from fields that are not mutable; or
  • You can ensure that the hash code of a mutable object does not change while the object is contained in a collection that relies on its hash code.*

But why do you need to override GetHashCode() in the first place? Normally, you will do it if your object is going to be used in a Hashtable, as a key in a dictionary, etc., and it's quite hard to predict when your object will be added to a collection and how long it will be kept there.

With all that said, if you want to be on the safe side make sure that your override of GetHashCode() returns the same value during the object's lifetime. ReSharper will help you here by pointing at each non-readonly field or non-get-only property in your implementation of GetHashCode(). If possible, ReSharper will also suggest quick-fixes to make these members read-only/get-only.

当然,如果 quick-fixes 不可能,它不建议做什么。但是,它确实表明那些 quick-fixes 应该只使用 "if possible",这意味着可以禁止检查。 Gian Paolo 对此的回答建议抛出一个异常,这将阻止 class 被用作密钥,并且如果它被无意中用作密钥,则会在开发早期出现。

但是,GetHashCode 用于其他情况,例如将对象的实例作为参数传递给模拟方法设置时。因此,唯一可行的选择是使用可变值实现 GetHashCode 并将责任放在其余代码上,以确保对象在用作键时不发生变化,或者不将其用作一把钥匙。