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

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

提问人:Neo 提问时间:3/2/2018 最后编辑:Neo 更新时间:3/16/2018 访问量:2897

问:

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

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

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

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

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

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

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

c# 不变性 可变 gethashcode 可依吉奥

评论

0赞 Hans Passant 3/2/2018
这是胡说八道。仅当对象用作字典中的键或存储在哈希集中时才出现问题。如果对象在存储后发生突变,则 GetHashCode 不应返回其他值。也许您必须通过实际提供自定义 GetHashCode :)来强制执行某些操作
0赞 Neo 3/2/2018
@HansPassant我同意,但自定义必须仅使用不可变属性。否则,使用默认的,根据我问题的最后一段,它仍然允许 a 正常工作,但没有它通常具有的 O(1) 性能。GetHashCodeDictionary<TKey, TValue>
0赞 Gian Paolo 3/2/2018
@Neo,不,使用字典的默认实现将无法正常工作。请参阅示例GetHashCode()
0赞 Neo 3/2/2018
@GianPaolo 然后,在类型没有不可变属性的情况下,这被认为是期望的行为,或者有一个解决方案可以覆盖。在此示例中,您将如何实现?GetHashCodeGetHashCode

答:

0赞 Karolis Kajenas 3/2/2018 #1

这完全取决于你说的是哪种类型。对于我的回答,我将假设您正在谈论基于,特别是我将针对 .NET 和计算解决它。collectionHash TablecollectionsDictionaryKey

因此,确定修改时会发生什么(假设您是一个执行自定义计算的类)的最佳方法是查看 .NET 源代码。从.NET源代码中,我们可以看到你的现在被包装成结构体,该结构体携带的值是计算的。这意味着,如果在添加键的时间之后更改值,它将无法再在 中找到值。keykeyHashCodekey value pairEntryhashcodeadditionHashCodedictionary

证明这一点的代码:

    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 源引用。

所以要回答你的问题。您希望具有计算所依据的不可变值。hashcode

还有一点,对于自定义类,如果不覆盖将基于 .因此,对于基础值相同的不同对象返回相同的问题,可以通过方法和根据业务键计算来减轻。例如,您将有两个字符串属性,用于计算哈希码和调用基方法。这将保证 的相同基础值相同。hashcodeGetHashCodeobjecthashcodeoverriding GetHashCodeHashCodeconcatstringsstringGetHashCodehashcodeobject

评论

0赞 Neo 3/2/2018
感谢您的回答。我基本上同意你的看法,但我不同意你应该添加不可变属性纯粹是为了在 中使用。在这种情况下,您不妨只使用 which 将始终返回相同的值,并且不需要覆盖。GetHashCodebase.GetHashCode
0赞 Karolis Kajenas 3/2/2018
@Neo 请看一下答案最后一部分的补充。默认情况下,类上的 GetHashCode 基于对象的引用。如果覆盖它,则可以控制实际确定对象哈希码的基础值。如果最后一部分直接解决了您的问题,请告诉我。
0赞 Neo 3/2/2018
我读完最后一段后回答说。我同意你最后的评论,但这并不能解决我所说的问题。我不认为你应该纯粹为了 .默认实现将达到相同的目的。您将获得独特的价值。GetHashCode
2赞 Gian Paolo 3/2/2018 #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;
    }
}

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 一起使用时,您不能自由修改类。在某些情况下,这是可以接受的,而在其他情况下则不然

评论

0赞 Neo 3/2/2018
有人对这个答案投了反对票。我投了赞成票,因为我认为它最能解决我的问题。我在这里找到了一个很好的总结。我特别喜欢这个评论和它所评论的答案。这让我一分钱一分货。 仍应用于没有不可变属性的类型,但它要么不应用作键,要么在用作键后不应更改。GetHashCode
1赞 Vapid 10/24/2018
在 GetHashCode 中抛出异常通常看起来是一个可怕的想法,除非你有非常非常好的理由不允许该对象包含在字典中。最好只返回 0
2赞 Gian Paolo 10/24/2018
@VapidLinus,我认为对于可变对象来说,没有“完美”的解决方案:你必须放弃字典的至少一个“预期”行为。返回常量是 GetHashCode 的“合法”实现,但它向调用方隐藏了 Dictonary/Hashtable/HashSet/Whatever 不会像他预期的那样快。您需要根据当前方案做出选择,在某些情况下,抛出异常是一个需要考虑的选项。顺便说一句,返回 0 它实际上是我经常使用的东西。
-3赞 Neo 3/2/2018 #3

经过大量讨论并阅读了有关该主题的其他 SO 答案,最终是这个 ReSharper 帮助页面为我总结得很好:

GetHashCode() 方法的 MSDN 文档并不明确要求重写此方法返回在对象生存期内永不更改的值。具体来说,它说:

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

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

*您可以覆盖不可变引用类型的 GetHashCode。通常,对于可变引用类型,仅当满足以下条件时才应重写 GetHashCode:

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

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

综上所述,如果您想安全起见,请确保重写 GetHashCode() 在对象的生命周期内返回相同的值。ReSharper 将通过指向 GetHashCode() 实现中的每个非只读字段或非 get only 属性来帮助您。如果可能的话,ReSharper 还会建议快速修复,使这些成员成为只读/只获取。

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

但是,在其他情况下使用,例如,当对象的实例作为参数传递给模拟方法设置时。因此,唯一可行的选择是使用可变值实现,并将责任放在代码的其余部分,以确保对象在用作键时不会发生突变,或者根本不将其用作键。GetHashCodeGetHashCode