Monday, January 30, 2012

Defining custom Dictionary keys in C#

Ever so often we come across cases where we need to store objects mapped to other objects in a Dictionary. And in most of these cases, the default solution of using the object reference as a key doesn’t really work. For example, if an object Person { Name = “Foo” } is mapped to value ‘x’, another object Person { Name = “Foo” } does not get us to the value ‘x’, although we probably would expect it to return ‘x’.
class Person    
{     
    public int Age { get; set; }     
    public string Name { get; set; }     
}

class Program    
{     
    static void Main(string[] args)     
    {     
        var p1 = new Person() { Age = 30, Name = "p1" };     
        var p2 = new Person() { Age = 31, Name = "p2" };     
        var p3 = new Person() { Age = 32, Name = "p3" };
        var personVsBonus = new Dictionary<Person, int>();    
        personVsBonus[p1] = 1000;     
        personVsBonus[p2] = 2000;     
        personVsBonus[p3] = 3000;
        var personToFind = new Person() { Age = 31, Name = "p2" };    
        Console.WriteLine(personToFind.Name + " will be given a bonus of " + personVsBonus[personToFind]);     
    }     
}     

The above program gives throws a KeyNotFoundException as the dictionary was not able to locate the entry searched as it was doing a reference comparison.
In such cases you need to define your own implementation of what the dictionary should consider as the key value. In the example we just discussed, we might like the property Person.Name to be considered as the key. Whenever a dictionary needs to decide where an item should be located, it calls GetHashCode(), whose return value decides where the item shall be stored. If GetHashCode() returns the same value for two different objects, the Equals() method is called with both objects being compared. The result of the Equals() method tells the dictionary whether the objects are the same or different.


Solution 1:
Implement the IEquatable interface for the Person class. This means that, “Hey, now the Person object will tell you whether it is the same as another Person object!”. You also need to override the Object.GetHashCode() method and write a good hash generating algorithm that generates unique values (int). Here, I use a simple method that just returns the length of the person’s name.
    class Person : IEquatable<Person>
    {
        public int Age { get; set; }
        public string Name { get; set; }
 
        #region IEquatable<Person> Members
 
        public bool Equals(Person other)
        {
            return this.Age.Equals(other.Age) 
                && this.Name.Equals(other.Name);
        }
 
        #endregion
 
        public override int GetHashCode()
        {
            return this.Name.Length;
        }
    }

Run the program after making these additions and the program works fine. The dictionary is now able to find appropriate values.

Solution 2:
There will be times when the key type might be a sealed type or you might not be allowed to change the key object’s properties. The Dictionary type provides us the IEqualityComparer interface that we can implement to provide our custom definitions based on the key object’s properties without having to modify the key’s properties.

Here is the modified code:

    class Person
    {
        public int Age { get; set; }
        public string Name { get; set; }
    }
 
    class PersonEqualityComparer : IEqualityComparer<Person>
    {
        #region IEqualityComparer<Person> Members
 
        public bool Equals(Person x, Person y)
        {
            return x.Age.Equals(y.Age)
                && x.Name.Equals(y.Name);
        }
 
        public int GetHashCode(Person obj)
        {
            return obj.Name.Length;
        }
 
        #endregion
    }

Also remember to use the comparer object while creating the dictionary.

var personVsBonus = new Dictionary<Person, int>(new PersonEqualityComparer());

Run the program after making these changes and the program works just fine!

The most important thing to remember here is that your definition of GetHashCode() shall decide how fast your dictionary can lookup an entry. Be very careful while taking these solutions into consideration as weak hash generation might lead you to long-term scalability issues where the dictionary might take more and more time to lookup entries in large dictionaries.

No comments:

Post a Comment