Monday, October 08, 2007

Unexceptional Dictionary Accesses in C#

Take a look at this:

            Dictionary<string, string> dictionary = new Dictionary<string, string>();

            dictionary["Hello"] = "World!";

            Console.WriteLine(dictionary["Hello"]);

            Console.WriteLine(dictionary["Goodbye"]);


What's the output of this code? You might think that you'd get the word "World!" followed by a blank line, but that's wrong. You get this big hairy exception:

System.Collections.Generic.KeyNotFoundException: The given key was not present in the dictionary.
at System.ThrowHelper.ThrowKeyNotFoundException()
at System.Collections.Generic.Dictionary`2.get_Item(TKey key)

I don't really like that exception at all. I find that it's usually quite unexceptional for something to be missing from a dictionary. You might think it would be nice to get a null back instead of getting an exception. I might agree with you. OK, but maybe they had a reason for making the dictionary behave this way. Think about this situation:

            Dictionary<int, int> dictionary = new Dictionary<int, int>();

            dictionary[1] = 2;

            Console.WriteLine(dictionary[1]);

            Console.WriteLine(dictionary[2]);


Now, what do you think? Still want a null back? Integers aren't reference types, and you should never be getting null when something is supposed to return an integer. You might think that could just return default(T) but in this case the return value would be zero. But, zero is a perfectly valid number so it probably would have been a bad design decision to have the dictionary return default(T). So we're left with this exception.

You might attempt to fix this by guarding all your dictionary accesses with ContainsKey() checks like so:

            Dictionary<string, string> dictionary = new Dictionary<string, string>();

            dictionary["Hello"] = "World!";

            if (dictionary.ContainsKey("Hello"))

                Console.WriteLine(dictionary["Hello"]);

            if (dictionary.ContainsKey("Goodbye"))

                Console.WriteLine(dictionary["Goodbye"]);


There's a few of problems with this approach, however. First of all, it looks ugly, and it's really verbose. What if you're retrieving a large number of values from this dictionary? All those ifs get really tedious, and there's more potential for error when you're providing the key twice to the dictionary. Also, there's a potential race condition here. What if another thread removes the key from the dictionary the instant after you confirm that it is in fact in the dictionary, and then try to retrieve the value for the key? You're going to get an exception. And yet another problem is that this solution is inefficent. The dictionary has having to perform the work to lookup the key twice instead of just once.

So if you can't guard the dictionary access with an if, how can you safely determine if a key is in a dictionary and retrieve its value while avoiding a race condition? Well, they thought of that. That's why they created TryGetValue(), which basically works like this:

            Dictionary<string, string> dictionary = new Dictionary<string, string>();

            dictionary["Hello"] = "World!";

            string value;

            if (dictionary.TryGetValue("Hello", out value))

            {

                Console.WriteLine("Dictionary has the key!");

                Console.WriteLine("The value is " + value);

            }

            else

            {

                Console.WriteLine("Dictionary doesn't have the key!");

            }


So this solves that latter two problems of the three problems I mentioned above about the guard clauses, but it really exacerbates the first problem of verbosity. So how can we fix this? What if you really just want to get an null back if the key was missing from the dictionary? As mentioned before, you'd only want to do this if your value type was a reference type. How would you get about it? You could create a helper function like this:

        public string GetValueSafelyFromDictionary(Dictionary<string, string> dictionary, string key)

        {

            string value;

            if (dictionary.TryGetValue(key, out value))

                return value;

            return null;

        }


Or more generally:

        public TValue GetValueSafelyFromDictionary<TKey, TValue>(Dictionary<TKey, TValue> dictionary, TKey key) where TValue : class

        {

            TValue value;

            if (dictionary.TryGetValue(key, out value))

                return value;

            return null;

        }


So this could definitely helps cut down on the verbosity, especially if the value from dictionary is supposed to go into the property of another reference type instead of some variable on the stack. But it's quite annoying to have to keep passing the dictionary, so let's make this better:

    public class DictionaryValueGetter<TKey, TValue> where TValue : class

    {

        private readonly IDictionary<TKey, TValue> _dictonary;

 

        public DictionaryValueGetter(IDictionary<TKey, TValue> dictonary)

        {

            _dictonary = dictonary;

        }

 

        public TValue this[TKey key]

        {

            get

            {

                TValue value;

                if (!_dictonary.TryGetValue(key, out value))

                    return null;

                return value;

            }

        }

    }


OK, this is a lot better. Take a look:

            Dictionary<string, string> dictionary = new Dictionary<string, string>();

            dictionary["Hello"] = "World!";

            DictionaryValueGetter<string, string> valueGetter = new DictionaryValueGetter<string, string>(dictionary);

            Console.WriteLine(valueGetter["Hello"]);

            Console.WriteLine(valueGetter["Goodbye"]);


And this will work with any dictionary where your "value" type is a reference type, which is perfect because that's the only time this solution makes any sense. We could have gone further than this solution and provided a full implementation of IDictionary that provided all of this behavior, but I think that was a bit too much work for my simple use case.

No comments: