.NET Dictionary 实现如何与可变对象一起使用

Posted

技术标签:

【中文标题】.NET Dictionary 实现如何与可变对象一起使用【英文标题】:How .NET Dictionary implementation works with mutable objects 【发布时间】:2013-02-28 11:43:41 【问题描述】:

我了解不建议使用“可变”对象(其 GetHashCode() 方法在用作Dictionary 中的键时可以返回不同结果的对象)。

以下是我对实现为哈希表的字典如何工作的理解:

当我添加新密钥时,例如dict.Add(m1, "initially here was m1 object");dict 使用GetHashCode() 方法计算m1 的哈希码。然后它做一些内部计算,最后把这个对象放到它内部数组的某个位置。

当我使用键索引获取值时,例如dict[m1]dict 再次计算哈希码。然后它进行一些内部计算,它给了我一个位于其内部数组内部计算位置的对象。

但我认为有一个我找不到的错误。

假设我有这个代码:

    class MutableObject
    
         Int32 m_value;

         public MutableObject(Int32 value)
         
             m_value = value;
         

         public void Mutate(Int32 value)
         
             m_value = value;
         

         public override int GetHashCode()
         
             return m_value;
         
   

   static void Main(string[] args)
   
         MutableObject m1 = new MutableObject(1);
         MutableObject m2 = new MutableObject(2);

         var dict = new Dictionary<MutableObject, String>();
         dict.Add(m1, "initially here was m1 object");
         dict.Add(m2, "initially here was m2 object");

         Console.WriteLine("Before mutation:");
         Console.WriteLine("dict[m1] = " + dict[m1]);
         Console.WriteLine("dict[m2] = " + dict[m2]);

         m1.Mutate(2);
         m2.Mutate(1);

         Console.WriteLine("After mutation:");
         Console.WriteLine("dict[m1] = " + dict[m1]);
         Console.WriteLine("dict[m2] = " + dict[m2]);

         Console.ReadKey(true);
   

当我调用 Mutate 方法时,键被交换。所以我认为它会给出交换的结果。但实际上这一行:Console.WriteLine("dict[m1] = " + dict[m1]); 抛出 KeyNotFoundException,我不明白为什么。显然我在这里遗漏了一些东西......

【问题讨论】:

只有 40 亿个可能的哈希码。假设运气不好,您在同一个哈希表中有两个不相等的对象,具有相同的哈希码。 字典如何区分它们?当您回答这个问题时,您就会明白为什么您的程序会为您提供您所看到的结果。 我知道 hasttable 可以实现链接,或者开放寻址算法,例如,对抗碰撞。所以当我调用dict[m1] 他得到哈希码,交换后等于2,然后......他做了什么?他试图弄清楚,具有此哈希码的哪个对象是正确的,他应该返回哪个对象...... 你在 1 Elm Street 买房子,然后在 2 Elm Street 买房子并搬到那里。您将第二所房子的地址标志更改为 1 Elm Street。发往 1 Elm Street 的邮件在哪里投递? 很好的模拟,谢谢:) 【参考方案1】:

仅仅通过字典查找来获得相同的哈希码是不够的。由于可能发生哈希冲突,因此键也必须与正在查找的索引相同。

【讨论】:

即使有这样的改变,它也不起作用。 Dictionary 假定添加到其中的项目在字典中时不会更改其哈希码。【参考方案2】:

您的MutableObject 类不会覆盖Equals(object)。因此使用了引用相等(继承自基类System.Object)。

Dictionary&lt;,&gt; 首先(快速)找到具有正确哈希码的任何键。然后,它检查每个候选键以检查其中一个 Equals 是否是它正在搜索的键。

因此Equals(object)GetHashCode() 应该一起被覆盖。如果您只覆盖其中一个,您会从编译器收到警告

当密钥在Dictionary&lt;,&gt; 中时,一旦密钥的哈希码发生突变,该密钥将(可能)错位在Dictionary&lt;,&gt; 中,位于错误的“存储桶”中,因此会丢失。它不会被找到,因为搜索它总是在它不存在的存储桶中进行。

在本例中,密钥丢失,因此可以再次添加:

var dict = new Dictionary<MutableObject, string>();

var m = new MutableObject(1);

dict.Add(m, "Hello");

m.Mutate(2);

dict.Add(m, "world");

foreach (var p in dict)
    Console.WriteLine(p);

var otherDict = new Dictionary<MutableObject, string>(dict); // throws

在使用现有Dictionary&lt;,&gt; 中的项目初始化一个Dictionary&lt;,&gt; 期间,我实际上看到了这样的异常(两者都使用默认的EqualityComparer&lt;&gt; 作为密钥类型)。

【讨论】:

【参考方案3】:

.NET Dictionary 实现如何处理可变对象

它没有。 documentation for Dictionary 声明:

只要将对象用作Dictionary&lt;TKey, TValue&gt; 中的键,它就不能以任何影响其哈希值的方式发生变化。

由于您在 Dictionary 中更改对象,所以它不起作用。

至于为什么,不难看出。我们放入一个对象。假设哈希码是1。我们将对象放入哈希表的1 桶中。现在该对象从字典外部发生了变异,因此它的值(和哈希码)是2。现在,当有人将该对象提供给字典的索引器时,它会获取哈希码,看到它是2,并在2 存储桶中查找。那个桶是空的,所以它说,“对不起,没有元素”。

现在让我们假设创建了一个新对象,其值和哈希值为1。它被传递给 Dictionary,它看到哈希是 1。它在1 存储桶中查找,发现该索引处确实有一个项目。它现在使用Equals 来确定对象是否实际上相等(或者这是否只是哈希冲突)。

现在,在您的情况下,它会在这里失败,因为您没有覆盖Equals,您使用的是比较引用的默认实现,并且由于这是一个不同的对象,它不会有相同的引用。但是,即使您将其更改为比较值,*第一个对象也已变异为具有 2 的值,而不是 1,因此无论如何它都不会匹配。其他人建议修复此Equals 方法,您确实应该这样做,但它仍然无法解决您的问题

一旦对象发生变异,找到它的唯一方法就是发生变异值是哈希冲突(这是可能的,但不太可能)。如果不是,那么根据Equals 相等的任何东西都不会知道检查正确的桶,并且根据Equals 检查正确的桶的任何东西都不会相等。

我在开头提到的引用不仅仅是一个最佳实践。对字典中的项目进行变异不仅是出乎意料的、奇怪的或表现不佳的。 它只是不起作用

现在,如果对象是可变的但在字典中没有发生变异,那很好。这可能有点奇怪,这是人们可能会说这是不好的做法,即使它有效。

【讨论】:

这是一个很好的解释。谢谢你 有趣的注意:你提到的“哈希冲突”只有当变异值与原始值具有相同的哈希码时才会发生,如果两者都落入同一个桶中是不够的(因为Dictionary比较比较相等之前的完整哈希码)。 @svick 这就是我所指的,但我没有深入探讨这个例子,因为它似乎不值得解释。

以上是关于.NET Dictionary 实现如何与可变对象一起使用的主要内容,如果未能解决你的问题,请参考以下文章

数据类型与参数

ConcurrentDictionary 与 Dictionary

面试官:实现一个带值变更通知能力的Dictionary

ConcurrentDictionary与Dictionary 替换

可变于不可变对象分类

如何将 appsetting.json 部分加载到 .NET Core 中的 Dictionary 中?