Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Inconsistent behaviour in Dictionary/IDictionary

Take the following piece of code

        var dictionary = new Dictionary<string, int>
        {
            ["A"] = 1,
            ["B"] = 2,
            ["C"] = 3,
        };

        var dictionaryx = (IDictionary)dictionary;

        var x = dictionaryx["X"]; // return null
        var y = dictionary["Y"];  // throws KeyNotFoundException

It is interesting to see the element access through the indexer leads to two different behaviours in x and y. As in general, interfaces should give access to the same internal implementation. but in this case, it is different.

what is the catch here?

like image 207
Sency Avatar asked Jan 25 '23 06:01

Sency


2 Answers

That's because Dictionary<TKey, TValue> is implementing both the generic version of IDictionary<TKey, TValue> and the non-generic version (just IDictionary that yields an Object).

Looking at the source, you can easily see that the indexer of the generic version explicitly throws when the key is not found:

public TValue this[TKey key]
{
    get
    {
        int i = FindEntry(key);
        if (i >= 0) return entries[i].value;
        ThrowHelper.ThrowKeyNotFoundException(); // <-- this line
        return default(TValue);
    }
}

But the non-generic version does not:

object IDictionary.this[object key]
{
    get
    {
        if (IsCompatibleKey(key))
        {
            int i = FindEntry((TKey)key);
            if (i >= 0)
                return entries[i].value;
        }
        return null;
    }
}

Then, unless you explicitly cast your Dictionary to the non-generic version, you're calling the indexer that throws when the key is not found.

Update (as per your comment):

Since IDictionary (the non-generic version) was introduced in .NET 1.0 (when there was no support for generics yet) the only type it consumed and returned is obviously System.Object (which can be tested for null).

Hence, it made sense not to throw when key is not found because one could simply do this:

dict["nonExistingKey"] != null

But, once the generic IDictionary<TKey, TValue> was introduced, the TValue can also be set to a value type (like int or DateTime) and the above pattern would have to be adjusted to:

var dict = new Dictionary<string, int>();
dict["nonExistingKey"] != 0;
dict["nonExistingKey"] != default(int);

Which is less fluent, less standard and more cumbersome to use.

From "design perspective", I presume they decided to preserve backward compatibility for IDictionary while proactively throwing for the generic version.

like image 81
haim770 Avatar answered Jan 28 '23 14:01

haim770


System.Collections.Generic.Dictionary<K,V> implements many interfaces.

It implements System.Collections.Generic.IDictionary<K,V>, which specifies that access to non-existing keys using the indexer property should throw.

It also implements System.Collections.IDictionary, which specifies that access to non-existing keys using the indexer property should return null.

The default indexer of the Dictionary class is the first one - from System.Collections.Generic.IDictionary<K,V>. When you cast to IDictionary, you say you want the second behaviour.

Also, note that ((IDictionary)dictionary)["key"] returns an object, while dictionary["key"] returns an int.

like image 36
gnud Avatar answered Jan 28 '23 13:01

gnud