Pages

Thursday, August 04, 2022

System.Collections.IDictionary versus System.Collections.Generic.IDictionary

I've run into there being two different sets of collections in .NET before, generic vs. well, vs. not (?), but never really sat down to understand it.

They are very different. From stackoverflow.com:

ICollection<T> and ICollection are actually very different interfaces that unfortunately share a name and not much else.

Ugh.

From an MSDN link in that answer:

Collection<T> seems like ICollection, but it’s actually a very different abstraction. We found that ICollection was not very useful. At the same time, we did not have an abstraction that represented an read/write non-indexed collection. ICollection<T> is such abstraction and you could say that ICollection does not have an exact corresponding peer in the generic world; IEnumerable<T> is the closest.

Here they are:

This means LINQ works on the latter but not the former. Luckily ICollection (no T) is still enumerable, so foreach away! (Are there considerations about iteration, like can you do it twice? Maybe? I haven't run into one yet.)

There are similar issues with many collection types, reviewed in some detail on that page, including IDictionary...

From microsoft.com:

IDictionary<TKey, TValue> is roughly equivalent to IDictionary.

Which includes a quick follow-up on DictionaryBase:

Important

We don't recommend that you use the DictionaryBase class for new development. Instead, we recommend that you use the generic Dictionary<TKey,TValue> or KeyedCollection<TKey,TItem> class . For more information, see Non-generic collections shouldn't be used on GitHub.

That said, DictionaryBase's entry gives us a nice example implementation so we don't have to dig up a concrete implementation somewhere else, like Exception.Data (which is what created this rabbit hole for me).

Using that example ShortstringDictionary we can illustrate our issue this way:

ShortstringDictionary oldStyleDictionary = new ShortstringDictionary();

oldStyleDictionary.Add("One", "a");
oldStyleDictionary.Add("Two", "ab");
oldStyleDictionary.Add("Three", "abc");
oldStyleDictionary.Add("Four", "abcd");
oldStyleDictionary.Add("Five", "abcde");

//var filtered = oldStyleDictionary.Where( // <<<<< doesn't exist; no LINQ

So notice that there's no LINQ here, so we can't Where our ShortstringDictionary.

But the dictionary IS enumerable. Well, kinda. The Keys and Values collections are.

So we can foreach through Keys and, here, insert each key/value into a System.Collections.Generic.Dictionary<TKey,TValue> (here, specifically, a Dictionary<string, string>).

Note that the Keys collections' values, though the implementation forces them to be strings, are NOT typed!

Keys is a System.Collections.ICollection (from the humourously named System.Collections.NonGeneric.dll).

Dictionary<string, string> newStyleDictionary = new Dictionary<string, string>();
foreach (object key in oldStyleDictionary.Keys)
{
    string keyAsString = key.ToString();
    Console.WriteLine($"{key}: {oldStyleDictionary[keyAsString]}");

    newStyleDictionary.Add(keyAsString, oldStyleDictionary[keyAsString]);
}

Console.WriteLine("\n==========================\n");

Now our generic Dictionary<TKey,TValue> can use LINQ's Where to filter.

var filtered = newStyleDictionary.Where(x => x.Value.Contains("c"));

foreach (var kvp in filtered)
{
    Console.WriteLine($"{kvp.Key}: {kvp.Value}");
}

Here endeth the lesson.

And begineth another -- quick update from this SO answer:

The most painful difference is that for the generic Dictionary<string, string>, when I call this[key] for a key that does not exist, I get an exception stating the key does not exist.

On a DictionaryBase I get back null with no exception. This was painful in my case because the system was full of code that did not check that the dictionary ContainsKey before trying to get the keys value. It was made more painful by me assuming I messed something up with serialization.

That appears to be true!

Console.WriteLine($"{bogusKey}: {oldStyleDictionary[bogusKey]}"); // writes "bogus: "
Console.WriteLine($"{bogusKey}: {newStyleDictionary[bogusKey]}"); // throws System.Collections.Generic.KeyNotFoundException