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

Neo*_*Neo 8 c# mutable immutability iequatable gethashcode

我正在实施IEquatable<T>,但我很难就可变GetHashCode类的覆盖达成共识。

以下资源都提供了一个实现,GetHashCode如果对象发生更改,则在对象的生命周期内将返回不同的值:

但是,此链接指出GetHashCode不应为可变类型实现,因为如果对象是集合的一部分,则可能会导致不良行为(这也一直是我的理解)

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

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

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

输入此问题后,“类似问题”框中弹出了另一个问题,似乎涉及同一主题。答案似乎非常明确,因为在GetHashCode实现中只应使用不可变属性。如果没有,那就干脆不写。Dictionary<TKey, TValue>尽管不是 O(1) 性能,但仍然可以正常运行。

Gia*_*olo 2

可变类与字典和其他依赖 GetHashCode 和 Equals 的类一起工作非常糟糕。

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

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

    public override int GetHashCode()
    {
        return 0;
    }
}
Run Code Online (Sandbox Code Playgroud)

或者

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");
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

  • @VapidLinus,我认为对于可变对象没有“完美”的解决方案:您将不得不放弃至少一种字典的“预期”行为。返回常量是 GetHashCode 的“合法”实现,但它向调用者隐藏了 Dictonary/Hashtable/HashSet/Whatever 不会像他预期的那么快。您需要根据当前场景做出选择,抛出异常是某些情况下需要考虑的选项。顺便说一句,返回 0 实际上是我经常使用的东西。 (2认同)