Is Dictionary broken or should GetHashCode() only base on immutable members?

.net, c#, dictionary, hashcode, immutability

Solution

No, you just shouldn't mutate a key (in a material way) after inserting it into a dictionary. This is by design, and the way that every hash table I've ever used works. The docs even specify this:

As long as an object is used as a key in the `Dictionary<TKey, TValue>`, it must not change in any way that affects its hash value. Every key in a `Dictionary<TKey, TValue>` must be unique according to the dictionary's equality comparer. A key cannot be null, but a value can be, if the value type TValue is a reference type.

So it's only going to surprise users who don't read documentation :)

Problem

When an object is added to the .NET System.Collections.Generic.Dictionary class the hashcode of the key is stored internally and used for later comparisons. When the hashcode changes after its initial insertion into the dictionary it often becomes "inaccessible" and may surprise its users when an existence check, even using the same reference, returns false (sample code below). The GetHashCode documentation says: The GetHashCode method for an object must consistently return the same hash code as long as there is no modification to the object state that determines the return value of the object's Equals method. So, according to the `GetHashCode` docs, the hashcode may change whenever equality-determining state is changed, yet the `Dictionary` implementation does not support this. Is the current .NET dictionary implementation broken in that it incorrectly ignore the hashcode allowances? Should `GetHashCode()` only be based on immutable members? Or, is there something else to break a possible false dichotomy? ``` 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); } } ```

Original source