Click here to Skip to main content
Click here to Skip to main content

Dictionary with a Custom Key

By , 13 Feb 2008
 

Introduction

Generic dictionaries are great, but sometimes the built-in data types just doesn't suffice as keys. Perhaps you want to use more than one value as key, or a data type that doesn't support comparing by default.

Expandability

Luckily, the .NET Framework is built in a way that easily allows expansion. By creating a class to use as key, and a comparer that the dictionary uses to compare the key values, you can put anything you like in the key.

The Key Class

First, let's create a class to use as key in a dictionary. As an example I will use a dictionary that will keep track of cars parked in a parking garage. The parking spaces are identified by a floor number and and parking space number for that floor, so the key will have two integer properties; floor and parkingSpace:

public class ParkingSpaceKey {

    private int _floor, _parkingSpace;

    public ParkingSpaceKey(int floor, int parkingSpace) {
        _floor = floor;
        _parkingSpace = parkingSpace;
    }

}

So far all you can do with the class is to create instances of it, but we will add a comparer to it so that we can use it with a dictionary.

The Comparer

The IEqualityComparer<T> interface is used to implement a comparer for a dictionary. It has two methods that you need to implement: Equals and GetHashCode.

The Equals method is used to compare two key values. It simply returns true if the values are equal.

The GetHashCode method returns a hash code value for a specific key value. The hash code is what the dictionary uses to divide all the items in the dictionary into buckets. That is what gives speed to the dictionary.

There is a lot you can read about how you create good hash keys, but there are just two things that you need to know to make a hash code that works reasonably well in most situations:

  1. The method must always return the same hash code for a specific key value.
  2. The hash codes should be evenly distributed, so that you get as many buckets as possible in the dictionary.

Let's look at two examples of implementations. The first just returns the same value all the time:

public int GetHashCode() { return 0; }

This fulfills the rule that a specific key value always gets the same hash code, so the hash code actually works. The distribution of the hash codes is terrible, though. The dictionary would only get a single bucket to store all items in.

The second implementation is the actual implementation of the GetHashCode method for the System.Int32 structure:

public int GetHashCode() { return this; }

The hash code for an integer is actually the integer itself, which satisfies the rule that the hash code is always the same for a value. When it comes to distribution, it does not only work, it's actually the ideal implementation. Every key value will get it's own bucket in the dictionary, so you will always have as many buckets as possible. (It's of course only possible to make an ideal implementation if the key doesn't contain more than 32 bits of data, as the hash code contains 32 bits of data. If you have more data in the key, some key values have to share the same hash code.)

Implementing the Comparer

For our parking space example, we will just take the two integer values and do an exclusive or between them to create a hash code. Perhaps not the best implementation possible, but certainly good enough for this example.

Now, let's create a class that implements the IEqualityComparer<T> interface, and put it in the ParkingSpaceKey class. By putting the comparer inside the key class, you get two advantages. It's obvious what the class does, and you have access to the private variables in the key class from the comparer class.

public class ParkingSpaceKey {

    private int _floor, _parkingSpace;

    public ParkingSpaceKey(int floor, int parkingSpace) {
        _floor = floor;
        _parkingSpace = parkingSpace;
    }

    public class EqualityComparer : IEqualityComparer<ParkingSpaceKey> {

        public bool Equals(ParkingSpaceKey x, ParkingSpaceKey y) {
            return x._floor == y._floor && x._parkingSpace == y._parkingSpace;
        }

        public int GetHasCode(ParkingSpaceKey x) {
            return x._floor ^ x._parkingSpace;
        }

    }

}

That's all it takes to make a custom key for a dictionary. There are no properties for reading the values that are stored in the key. You can add them if you like, but that isn't needed to use the class as a key. The comparer doesn't care what the key contains, it only needs to be able to compare key values and to get a hash code for a key.

Use the Custom Key Class

Now you can use the comparer when you create a dictionary:

Dictionary<ParkingSpaceKey, Car> garage = new Dictionary<ParkingSpaceKey, Car>(
    new ParkingSpaceKey.EqualityComparer());

It's crucial that you actually remember to create an instance of the comparer for the dictionary. Otherwise the dictionary will use the default way of comparing the key objects, which will be comparing the references. (That means that even if you create a key with the same values as a key in the dictionary, it can't be used to find it as they are separete object so they doesn't have the same reference.)

To use the dictionary, you simply create a key object with the values you need to access. Example:

ParkingSpaceKey key = new ParkingSpaceKey(4, 42);
if (garage.ContainsKey(key)) {
    Console.WriteLine("Parking space occupied.");
} else {
    Console.WriteLine("Parking space available.");
}

That all, folks.

Resources

Wikipedia: hash table
MSDN Library: IEqualityComparer interface
MSDN Library: Dictionary class

Points of Interest

I learned about this when I needed dictionaries to cache data for a web application. Some of the data is depending on several different variables, so I needed dictionaries with custom keys.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Guffa
Web Developer Byt Bil Nordic AB
Sweden Sweden
Member
A programmer with things like Atari BASIC, 6502 machine code, Compis Pascal, GFA Basic, 68000 assembler, Turbo Pascal, 80x86 assembler, ASP (VBScript) and VB6 in the baggage.
 
Currently employed to build applications mainly using ASP.NET (C#) and MS SQL Server.

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralMy vote of 5memberMikhail Tsennykh (devnoob)7 Mar '12 - 5:51 
Pretty good, will need it in future!
GeneralMy vote of 4membercdguenther28 Apr '11 - 12:21 
Told me exactly what I needed to know - more than I can say about MSDN.
GeneralThanks!memberZ.J.Liu19 Nov '09 - 14:00 
Thanks, and thank all comments here. I use this method in my job.
 
Zhijun Liu

GeneralError in compilationmemberrkb20 Feb '08 - 10:16 
I want to have a dictionary that returns a true from ContainsKey if the substring of the string being tested is in the dictionary. In other words if the dictionary contains "A", I want the ContainsKey to return true for "Abcd", "A1234", etc. So I have the code:
 
public class PrefixDictionaryComparer : IEqualityComparer<string>
{
public bool Equals(string x, string y)
{
if (x.Length > y.Lenth)
{
return x.StartsWith(y);
}
else
{
return y.StartsWith(x);
}
}
public int GetHasCode(string x)
{
return GetHasCode(x);
}
}
 
And I create the dictionary like:
 

Dictionary<string, int> customDictionary = new Dictionary<string, int>(new PrefixDictionaryComparer());
 

but this gives me errors:
 
Error 1 'CustomKeyDictionary.PrefixDictionaryComparer' does not implement interface member 'System.Collections.Generic.IEqualityComparer<string>.GetHashCode(string)' C:\Development\CustomKeyDictionary\Program.cs 8 18 CustomKeyDictionary
 
What am I doing wrong?
 
Thank you.
 
Kevin
 
Kevin Burton
rkevinburton@charter.net

GeneralRe: Error in compilationmemberrkb20 Feb '08 - 10:38 
I found the compilation problem. The code on the web site has a misspelling GetHasCode instead of GetHashCode.
 
Now I have a question. It seems that ContainsKey relies on the hash code returned from GetHashCode to determine if the keys exist and Equals is never called (at least for me). When would Equals be called? How can I intercept ContainsKey?
 
Thank you.
 
Kevin Burton
rkevinburton@charter.net

AnswerRe: Error in compilationmvpGuffa20 Feb '08 - 11:24 
rkb wrote:
It seems that ContainsKey relies on the hash code returned from GetHashCode to determine if the keys exist and Equals is never called (at least for me).

 
Yes. It first gets the hash code to look for a bucket. If there is a bucket with a matching hash code, it uses the Equals method to compare the values inside the bucket to find the correct item.
 
The dictionary doesn't work properly with the comparer that you have implemented. If you have a comparer that returns true for key values that have different hash codes, you will not find the items that match, as the dictionary is looking in the wrong bucket.
 
Also, you can have more than one key that compares equal to a specific key value. If you for example have a dicitonary that contains the keys "aa" and "ab", and look for the key "a" in the dictionary, both would compare equal, which contradicts how a dictionary works.
 
A dictionary is simply not suitable for what you are trying to do. Hash codes can not be used to reduce the number of comparisons you have to do to find the matching key values.
 
Despite everything, the person most likely to be fooling you next is yourself.

Generala few suggestionsmemberBillWoodruff18 Feb '08 - 15:14 
Hi,
 
I agree with some of the other comments that the article, with the current title, is a bit mis-leading since it suggests that there's something unusual about using reference types as keys. imho a stronger title would be something like "Solutions for access/comparability problems when using reference types as keys in generic dictionaries."
 
A little re-working, perhaps mentioning Marc's comment on using stucts as keys, and I think you have a fine contribution to our collective .NET corpus delecti Smile | :) But, it's already useful ... and appreciated !
 
sincerely, Bill
 
"The greater the social and cultural distances between people, the more magical the light that can spring from their contact." Milan Kundera in Testaments Trahis

GeneralNice articlememberMaxGuernsey18 Feb '08 - 12:00 
This is a good article. I would consider changing the name. Dictionary already supports whatever kind of key you want. This is really about using the Strategy Pattern to alter how Dictionary handles keys.
 
Max Guernsey, III
Managing Member, Hexagon Software LLC
http://www.hexsw.com
http://www.dataconstructor.com

Generalnon-genericsmembertack000018 Feb '08 - 11:03 
if you also implemented
 
Equals(ByVal x As Object, ByVal y As Object) as boolean
GetHashCode(ByVal obj As Object) as integer
readonly property Default as IEqualityCompaper(of T)
 
wouldn't you be able to use this on non-generic collections such as HashTable?
 
tack

AnswerRe: non-genericsmvpGuffa18 Feb '08 - 23:10 
For a non-generic collection you would implement the non-generic IEqualityComparer interface, which requires you to implement the Equals and GetHashCode methods only.
 
Despite everything, the person most likely to be fooling you next is yourself.

GeneralThis is goodmemberN a v a n e e t h14 Feb '08 - 22:11 
I have been helped many times by you in discussion boards and I always expected an article from you. This is great. Good idea. Thanks for posting.
 
All C# applications should call Application.Quit(); in the beginning to avoid any .NET problems.- Unclyclopedia

My Website | Ask smart questions

GeneralGood Articlemembermerlin98114 Feb '08 - 3:57 
Thanks for sharing how to do this.
 
Even though your article used value type objects, it was a good example of how to implement this if the objects were reference type instead.
 


~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Rhabot - World of Warcraft Bot
Atlantis WoW - Free Private World of Warcraft Server
Make long URLs short with NeatURL.net
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

GeneralRe: Good ArticlemvpGuffa14 Feb '08 - 8:24 
merlin981 wrote:
Good Article

 
Thanks. Smile | :)
 
merlin981 wrote:
Even though your article used value type objects, it was a good example of how to implement this if the objects were reference type instead.

 
Yes, you can put most anything in the key class just as long as it makes sense to compare the values. I didn't mention this in the article, I'll consider editing it in. Smile | :)
 
Experience is the sum of all the mistakes you have done.

GeneralKeyedCollectionmvpScott Dorman13 Feb '08 - 16:37 
Interesting article. Have you looked at the KeyedCollection<TKey, TItem> Generic Class
[^]?
 
Scott.
 
—In just two days, tomorrow will be yesterday.
—Hey, hey, hey. Don't be mean. We don't have to be mean because, remember, no matter where you go, there you are. - Buckaroo Banzai

[Forum Guidelines] [Articles] [Blog]

GeneralRe: KeyedCollectionmvpGuffa13 Feb '08 - 20:17 
Scott Dorman wrote:
Interesting article.

 
Thanks. Smile | :)
 
Scott Dorman wrote:
Have you looked at the KeyedCollection Generic Class
[^]?

 
Yes, I have used that a few times.
 
Experience is the sum of all the mistakes you have done.

Generalstruct vs. classprotectorMarc Clifton13 Feb '08 - 8:37 
In your example, since floor and parkingSpace are value types, you can create a struct to wrap them and use the struct as the key without having to implement a comparer. This works because structs are treated as value types, so the comparison is done by value rather than by reference (the default behavior if a class instance is the key).
 
Marc
 

GeneralRe: struct vs. classmvpGuffa13 Feb '08 - 9:18 
Marc Clifton wrote:
you can create a struct to wrap them

 
Yes, that is true.
 
Marc Clifton wrote:
and use the struct as the key without having to implement a comparer

 
You would still want to implement the comparer, though.
 
The default comparison uses the ValueType.Equals(object o) method, which means that one of the values needs to be boxed. The method checks if a binary comparison can be done, otherwise it will loop through all the members of the structs and box each pair of values before comparing them.
 
This means that each single comparison results in the creation of at least one new object and up to eight new objects (including an array of FieldInfo objects to access the members through reflection).
 
Experience is the sum of all the mistakes you have done.

GeneralRe: struct vs. classprotectorMarc Clifton13 Feb '08 - 9:40 
Guffa wrote:
You would still want to implement the comparer, though.

 
Interesting. Thanks for pointing that out (and your other comments as well).
 
Marc
 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web03 | 2.6.130516.1 | Last Updated 13 Feb 2008
Article Copyright 2008 by Guffa
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid