【问题标题】:Is Dictionary broken or should GetHashCode() only base on immutable members?Dictionary 是否损坏或 GetHashCode() 是否应仅基于不可变成员?
【发布时间】:2011-02-02 07:35:06
【问题描述】:

将对象添加到 .NET System.Collections.Generic.Dictionary 类时,键的哈希码会在内部存储并用于以后的比较。当哈希码在其初始插入字典后发生更改时,它通常会变得“不可访问”,并且当存在性检查(即使使用相同的引用)返回 false 时,它​​的用户可能会感到惊讶(下面的示例代码)。

GetHashCode 文档说:

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

因此,根据GetHashCode 文档,每当equality-determining 状态更改时,哈希码可能会更改,但Dictionary 实现不支持这一点。

当前的 .NET 字典实现是否因为它错误地忽略了哈希码允许而被破坏? GetHashCode() 应该只基于不可变成员吗?或者,是否还有其他东西可以打破可能的错误二分法?

class Hashable
{
    public int PK { get; set; }

    public override int GetHashCode()
    {
        if (PK != 0) return PK.GetHashCode();
        return base.GetHashCode();
    }

    public override bool Equals(object obj)
    {
        return Equals(obj as Hashable);
    }

    public virtual bool Equals(Hashable other)
    {
        if (other == null) return false;
        else if (ReferenceEquals(this, other)) return true;
        else if (PK != 0 && other.PK != 0) return Equals(PK, other.PK);
        return false;
    }

    public override string ToString()
    {
        return string.Format("Hashable {0}", PK);
    }
}

class Test
{
    static void Main(string[] args)
    {
        var dict = new Dictionary<Hashable, bool>();
        var h = new Hashable();
        dict.Add(h, true);

        h.PK = 42;
        if (!dict.ContainsKey(h)) // returns false, despite same reference
            dict.Add(h, false);
    }
}

【问题讨论】:

  • 如果字典支持改变键,它必须挂接到键中,并在 GetHashCode() 更改时以某种方式得到通知。这样做的意义听起来很大,可能简直是不可能的。
  • 我一直认为字典是基于诸如底层对象 id 或指针之类的东西(我还不确定 .NET 有),但你是对的 - HashCode 是一个完美的实现选择一本字典。
  • 其实这个设计是为了允许你使用更复杂的类型作为key;你只需要正确设计它们。 (例如,如果它使用对象指针,则由于将复杂类型的两个实例作为键,您最终可能会在字典中出现重复项,即使您正确设计了该复杂类型来表示,例如复合键)。

标签: c# .net dictionary immutability hashcode


【解决方案1】:

不,您只是不应该在将键插入字典后(以实质性方式)对其进行变异。这是设计使然,也是我曾经使用过的每个哈希表的工作方式。文档甚至指定了这一点:

只要将对象用作Dictionary&lt;TKey, TValue&gt; 中的键,它就不能以任何影响其哈希值的方式发生变化。根据字典的相等比较器,Dictionary&lt;TKey, TValue&gt; 中的每个键都必须是唯一的。如果值类型 TValue 是引用类型,键不能为空,但值可以为空。

所以它只会让不阅读文档的用户感到惊讶:)

【讨论】:

  • 好吧,我活该……下次会阅读所有文档:)。
  • 作为一个愚蠢的副作用,你可以随心所欲地改变对象,只要不改变哈希码(例如,哈希码只取决于实体的键,而不是它的值)
  • 乔恩,非常感谢you just shouldn't mutate a key ..。我已经阅读了太多关于将可变对象放入字典键的论点,以及这对Equals 的“正确”实现意味着什么。当真正的答案是你刚才所说的:不要改变键......
【解决方案2】:

为了补充 Jon 的回答,我只想强调您引用的文档的某个部分:

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

现在,你已经违反了规则。您更改了PK,这不会影响Equals 的结果(因为您有ReferenceEquals 在那里签入),但是您的GetHashCode 的结果确实 改变。这就是简单的答案。

采用更具概念性的方法,我认为您可以这样看待它:如果您的类型覆盖 EqualsGetHashCode 行为,那么您已经获得了这种类型的一个实例等于另一个实例意味着什么。事实上,您已经将它定义为Hashable 对象可以更改为完全不同的东西;即,不再像以前那样使用的东西(因为它的哈希码已经改变了)。

从这个角度考虑,在你做了dict.Add(h, true),然后你改变了h.PK,字典不再包含h引用的对象了。它包含一些 else (实际上并不存在于任何地方)。这有点像你定义的类型是一条已经蜕皮的蛇。

【讨论】:

  • 我认为你只是把事情复杂化了,人们很难理解你在这里实际写了什么。 Jon Skeet 说了这么多。
  • @Al:哈哈……抱歉?我以为我在说一些有意义的事情;如果您觉得这个答案荒谬或过于混乱,您可以随意投反对票;我个人不会接受。虽然我希望解释一下真正有问题的部分是什么,如果可能的话(即,我具体说了什么促使你写了那条评论)。也许我只是措辞不佳,可能会更清楚。
  • 我喜欢你以“采用更概念化的方法”开头的段落。我真的不喜欢规范中引用的部分,因为我看不到具有可见可变状态的类覆盖Object.Equals 的任何合法基础。等价的概念对于所有类型的对象(类和结构)都有很好的定义;对象应该覆盖Object.Equals 以测试等价性(即使某些类型(例如Decimal)覆盖Object.Equals 以表示其他含义),并且Object.GetHashCode() 应该与Object.Equals 兼容。
猜你喜欢
  • 1970-01-01
  • 2012-04-13
  • 2021-11-28
  • 2011-10-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多