【问题标题】:Should GetHashCode be implemented for IEquatable<T> on mutable types?是否应该在可变类型上为 IEquatable<T> 实现 GetHashCode?
【发布时间】:2018-03-01 17:43:04
【问题描述】:

我正在实现 IEquatable&lt;T&gt;,但我很难就可变类上的 GetHashCode 覆盖达成共识。

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

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

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

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

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

输入此问题后,another Question 会出现在似乎解决同一主题的“类似问题”框中。那里的答案似乎很明确,因为在GetHashCode 实现中应该只使用不可变的属性。如果没有,那就干脆不写一个。 Dictionary&lt;TKey, TValue&gt; 仍然可以正常运行,尽管性能不是 O(1)。

【问题讨论】:

  • 这是废话。仅当对象用作字典中的键或存储在哈希集中时才会出现问题。如果对象在存储后发生变异,则 GetHashCode 不应返回不同的值。也许您必须通过实际提供自定义 GetHashCode 来强制执行:)
  • @HansPassant 我同意,但自定义 GetHashCode 必须仅使用不可变属性。否则,请使用默认值,根据我的问题的最后一段,它仍然允许Dictionary&lt;TKey, TValue&gt; 正常运行,但没有它通常具有的 O(1) 性能。
  • @Neo,不,使用GetHashCode() 的默认实现,您的字典将无法正常工作。见this例子
  • @GianPaolo 那么要么这被认为是类型没有不可变属性的期望行为,要么有一个解决方案来覆盖GetHashCode。在这个例子中你将如何实现GetHashCode

标签: c# immutability mutable gethashcode iequatable


【解决方案1】:

可变类在字典和其他依赖于 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 (几乎)按预期工作,但在查找和插入时会降低性能:在这两种情况下,将对字典中已有的每个元素调用 Equals,直到比较返回 true。您实际上是在恢复列表的性能

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

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

【讨论】:

  • 有人否决了这个答案。我赞成,因为我认为它最能解决我的问题。我找到了一个很好的总结here。我特别喜欢this comment 和它正在评论的答案。这让我一分钱一分货。 GetHashCode 仍应用于没有不可变属性的类型,但它不应该用作键,或者一旦用作键就不应更改。
  • 在 GetHashCode 中抛出异常通常看起来是一个可怕的想法,除非您有非常非常好的理由不允许将对象包含在字典中。最好只返回 0
  • @VapidLinus,我认为可变对象没有“完美”的解决方案:您将不得不放弃至少一个字典的“预期”行为。返回常量是 GetHashCode 的“合法”实现,但它向调用者隐藏了 Diictonary/Hashtable/HashSet/Whatever 不会像他预期的那样快。您需要根据当前情况做出选择,并且在某些情况下可以考虑抛出异常。顺便说一句,返回 0 实际上是我经常使用的东西。
【解决方案2】:

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

因此,如果您修改 key(鉴于您的 key 是一个执行自定义 HashCode 计算的类),那么确定会发生什么的最佳方法是查看 .NET 源代码。从 .NET 源代码中,我们可以看到您的 key value pair 现在被包装到带有 hashcodeEntry 结构中,该结构是根据您的价值的 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,自定义类的hashcode 将基于object 的引用。因此,通过overriding GetHashCode 方法并根据您的业务密钥计算您的HashCode,可以减轻对基础值相同的不同对象返回相同hashcode 的担忧。例如,您将有两个字符串属性,要计算哈希码,您将 concat strings 并调用 base string GetHashCode 方法。这将保证对于object 的相同基础值,您将获得相同的hashcode

【讨论】:

  • 感谢您的回答。我基本同意你的观点,但我不同意你应该添加不可变属性纯粹用于GetHashCode。在这种情况下,您不妨只使用base.GetHashCode,它将始终返回相同的值,并且不需要覆盖。
  • @Neo 请看一下答案最后一部分的补充。默认情况下,类上的 GetHashCode 是基于对象的引用。如果您覆盖它,您可以控制哪些底层值实际决定了对象的哈希码。如果最后一部分直接解决了您的问题,请告诉我。
  • 我在阅读了最后一段后回复。我同意你最后的评论,但这并没有解决我所说的。我认为您不应该仅仅为了GetHashCode 而添加不可变属性。默认实现将达到相同的目的。你会得到一个独特的价值。
【解决方案3】:

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

GetHashCode() 方法的

MSDN documentation 没有明确要求您对此方法的覆盖返回一个在对象生命周期内永远不会改变的值。具体来说,它说:

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

另一方面,它表示至少当你的对象在一个集合中时,哈希码不应该改变:

*您可以为不可变引用类型覆盖 GetHashCode。一般来说,对于可变引用类型,只有在以下情况下才应该覆盖 GetHashCode:

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

但是为什么你首先需要覆盖 GetHashCode() 呢?通常,如果您的对象将在 Hashtable 中使用,作为字典中的键等,您会这样做,并且很难预测您的对象何时会添加到集合中以及它将在那里保存多长时间。

话虽如此,如果您想安全起见,请确保您对 GetHashCode() 的覆盖在对象的生命周期内返回相同的值。 ReSharper 将通过在您的 GetHashCode() 实现中指向每个非只读字段或非仅获取属性来帮助您。如果可能,ReSharper 还会建议 quick-fixes 将这些成员设为只读/只获取。

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-03-18
    • 1970-01-01
    • 1970-01-01
    • 2012-11-24
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多