.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<,>
首先(快速)找到具有正确哈希码的任何键。然后,它检查每个候选键以检查其中一个 Equals
是否是它正在搜索的键。
因此Equals(object)
和GetHashCode()
应该一起被覆盖。如果您只覆盖其中一个,您会从编译器收到警告。
当密钥在Dictionary<,>
中时,一旦密钥的哈希码发生突变,该密钥将(可能)错位在Dictionary<,>
中,位于错误的“存储桶”中,因此会丢失。它不会被找到,因为搜索它总是在它不存在的存储桶中进行。
在本例中,密钥丢失,因此可以再次添加:
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<,>
中的项目初始化一个Dictionary<,>
期间,我实际上看到了这样的异常(两者都使用默认的EqualityComparer<>
作为密钥类型)。
【讨论】:
【参考方案3】:.NET Dictionary 实现如何处理可变对象
它没有。 documentation for Dictionary 声明:
只要将对象用作
Dictionary<TKey, TValue>
中的键,它就不能以任何影响其哈希值的方式发生变化。
由于您在 Dictionary
中更改对象,所以它不起作用。
至于为什么,不难看出。我们放入一个对象。假设哈希码是1
。我们将对象放入哈希表的1
桶中。现在该对象从字典外部发生了变异,因此它的值(和哈希码)是2
。现在,当有人将该对象提供给字典的索引器时,它会获取哈希码,看到它是2
,并在2
存储桶中查找。那个桶是空的,所以它说,“对不起,没有元素”。
现在让我们假设创建了一个新对象,其值和哈希值为1
。它被传递给 Dictionary,它看到哈希是 1
。它在1
存储桶中查找,发现该索引处确实有一个项目。它现在使用Equals
来确定对象是否实际上相等(或者这是否只是哈希冲突)。
现在,在您的情况下,它会在这里失败,因为您没有覆盖Equals
,您使用的是比较引用的默认实现,并且由于这是一个不同的对象,它不会有相同的引用。但是,即使您将其更改为比较值,*第一个对象也已变异为具有 2
的值,而不是 1
,因此无论如何它都不会匹配。其他人建议修复此Equals
方法,您确实应该这样做,但它仍然无法解决您的问题。
一旦对象发生变异,找到它的唯一方法就是发生变异值是哈希冲突(这是可能的,但不太可能)。如果不是,那么根据Equals
相等的任何东西都不会知道检查正确的桶,并且根据Equals
检查正确的桶的任何东西都不会相等。
我在开头提到的引用不仅仅是一个最佳实践。对字典中的项目进行变异不仅是出乎意料的、奇怪的或表现不佳的。 它只是不起作用。
现在,如果对象是可变的但在字典中没有发生变异,那很好。这可能有点奇怪,这是人们可能会说这是不好的做法,即使它有效。
【讨论】:
这是一个很好的解释。谢谢你 有趣的注意:你提到的“哈希冲突”只有当变异值与原始值具有相同的哈希码时才会发生,如果两者都落入同一个桶中是不够的(因为Dictionary
比较比较相等之前的完整哈希码)。
@svick 这就是我所指的,但我没有深入探讨这个例子,因为它似乎不值得解释。以上是关于.NET Dictionary 实现如何与可变对象一起使用的主要内容,如果未能解决你的问题,请参考以下文章
ConcurrentDictionary 与 Dictionary