Ripal Barot
Ripal Barot

Reputation: 716

KeyNotFoundException in C# Dictionary after changing property value based on what, the GetHashCode is calculated. Why?

See the code below.

            static void Main(string[] args)
            {
    // Create Dictionary
                var dict = new Dictionary<TestClass, ValueClass>();

    // Add data to dictionary
                CreateSomeData(dict); 

    // Create a List
                var list = new List<TestClass>();
                foreach(var kv in dict) {
    // Swap property values for each Key
    // For example Key with property value 1 will become 6
    // and 6 will become 1
                    kv.Key.MyProperty = 6 - kv.Key.MyProperty + 1;

    // Add the Key to the List
                    list.Add(kv.Key);
                }

// Try to print dictionary and received KeyNotFoundException.
                foreach (var k in list)
                {
                    Console.WriteLine($"{dict[k].MyProperty} - {k.MyProperty}");
                }
            }



    static void CreateSomeData(Dictionary<TestClass, ValueClass> dictionary) {
        dictionary.Add(new TestClass {MyProperty = 1}, new ValueClass {MyProperty = 1});
        dictionary.Add(new TestClass {MyProperty = 2}, new ValueClass {MyProperty = 2});
        dictionary.Add(new TestClass {MyProperty = 3}, new ValueClass {MyProperty = 3});
        dictionary.Add(new TestClass {MyProperty = 4}, new ValueClass {MyProperty = 4});
        dictionary.Add(new TestClass {MyProperty = 5}, new ValueClass {MyProperty = 5});
        dictionary.Add(new TestClass {MyProperty = 6}, new ValueClass {MyProperty = 6});
    }

Key and Value Class:

namespace HashDictionaryTest
{
    public class TestClass
    {
        public int MyProperty { get; set; }

        public override int GetHashCode() {
            return MyProperty;
        }
    }

}

namespace HashDictionaryTest
{
    public class ValueClass
    {
        public int MyProperty { get; set; }

        public override int GetHashCode() {
            return MyProperty;
        }
    }

}

I am using the dotnet core 2.2 on Ubuntu. I did this test out of curiosity only. However, to my surprise, I got KeyNotFoundException.

I expected to receive the wrong values. However, I received the exception as mentioned above.

What I want to know is, why we got this error? What is the best practice in generating the HashCode so that we can avoid such issues?

Upvotes: 2

Views: 519

Answers (3)

radarbob
radarbob

Reputation: 5101

KeyNotFoundException ... Why?

The distilled, core reason is that the Equals and GetHashCode methods are inconsistent. This situation is fixed by doing 2 things:

  • Override Equals in TestClass
  • Never modify a dictionary during iteration
    • It's that the key object/value is being modified

GetHashCode - Equals disconnect


TestClass.Equals

I say TestClass because that is the dictionary key. But this applies to ValueClass too.

A class' Equals and GetHashCode must be consistent. When overriding either but not both then they are not consistent. We all know "if you override Equals also override GetHashCode". We never override GetHashCode yet seem to get away with it. Hear me now and believe me the first time you implement IEqualityComparer and IEquatable - always override both.


Iterating Dictionary

Do not add or delete an element, modify a key, nor modify a value (sometimes) during iteration.


GetHashCode

  • MSDN GetHashCode
    • Do not use the hash code as the key to retrieve an object from a keyed collection.
    • Do not test for equality of hash codes to determine whether two objects are equal

The OP code may not be doing this literally but certainly virtually because there is no Equals override.

Here is a neat hash algorithm from C# demi god Eric Lipper

Upvotes: 2

Eric Lippert
Eric Lippert

Reputation: 660503

What I want to know is, why we got this error?

There are guidelines for GetHashCode, and there are rules. If you violate the guidelines, you get lousy performance. If you violate the rules, things break.

You are violating the rules. One of the rules of GetHashCode is while an object is in a dictionary, its hash code must not change. Another rule is that equal objects must have equal hash codes.

You've violated the rules, and so things are broken. That's your fault; don't violate the rules.

What is the best practice in generating the HashCode so that we can avoid such issues?

For a list of the rules and guidelines, see:

https://ericlippert.com/2011/02/28/guidelines-and-rules-for-gethashcode/

Upvotes: 5

That is the expected behavior with your code. Then what is your wrong with your code?

Look at your Key Class. You are overriding your GetHashCode() and on top of that you are using a mutable value to calculate the GetHashCode() method (very very bad :( ).

public class TestClass
{
    public int MyProperty { get; set; }

    public override int GetHashCode() {
        return MyProperty;
    }
}

The look up in the implementation of the Dictionary uses GetHashCode() of the inserted object. At the time of insertion of your object your GetHashCode() returned some value and that object got inserted into some bucket. However, after you changed your MyProperty; GetHashCode() does not return the same value therefore it can not be look-ed up any more

This is where the lookup occurs

Console.WriteLine($"{dict[k].MyProperty} - {k.MyProperty}");

dict[k] already had its MyProperty changed therefore GetHashCode() does not return the value when the object first added to the dictionary.

Ant another really important thing is to keep in mind that when you override GetHashCode() then override Equals() as well. The inverse is true too!

Upvotes: 4

Related Questions