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

我知道不建议使用“可变”对象(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(); 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,我无法理解为什么。 显然我在这里遗漏了一些东西……

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

它没有。 字典的文档说明:

只要对象在Dictionary用作键,就不能以任何影响其哈希值的方式进行更改。

由于您在“ Dictionary更改对象时它将无法工作。

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

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

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

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

我在开始时提到的引用不仅仅是一种最佳实践。 在字典中改变项目不仅是意外的,也不是奇怪的或无法执行的。 它只是不起作用

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

使字典查找具有相同的哈希码是不够的。 由于哈希冲突是可能的,因此密钥也必须等于正在查找的索引。

您的MutableObject类不会重写Equals(object) 。 因此使用引用相等(从基类System.Objectinheritance)。

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

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

一旦密钥的哈希码在密钥在Dictionary<,>变异,该密钥将(可能)错误地放在Dictionary<,> ,在错误的“桶”中,因此丢失。 它将无法找到,因为搜索它将始终发生在它未找到的存储桶中。

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

 var dict = new Dictionary(); 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(dict); // throws 

在使用现有Dictionary<,>的项初始化一个Dictionary<,> ,我实际上已经看到了类似的exception(两者都使用键类型的默认EqualityComparer<> )。