4

I've recently just noticed that IDictionary does not implement IReadOnlyDictionary. I'm using two third-party libraries, one of which provides a ToDictionary() method which returns an IDictionary containing the contents, and another which consumes an IReadOnlyDictionary. In order to make this work, I've had to write the following ugly code:

// IDictionary does not implement IReadOnlyDictionary, so we have to call .ToDictionary()
// again to get a concrete dictionary implementation.
var errors = result.ToDictionary().ToDictionary(x => x.Key, x => x.Value);

From looking at the two interfaces, it seems that the methods in IReadOnlyDictionary are a direct subset of IDictionary:

// Represents a generic read-only collection of key/value pairs.
public interface IReadOnlyDictionary<TKey, TValue> : IEnumerable<KeyValuePair<TKey, TValue>>, IEnumerable, IReadOnlyCollection<KeyValuePair<TKey, TValue>>
{
    TValue this[TKey key] { get; }
    IEnumerable<TKey> Keys { get; }
    IEnumerable<TValue> Values { get; }
    bool ContainsKey(TKey key);
    bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value);
}

// Represents a generic collection of key/value pairs. public interface IDictionary<TKey, TValue> : ICollection<KeyValuePair<TKey, TValue>>, IEnumerable<KeyValuePair<TKey, TValue>>, IEnumerable { TValue this[TKey key] { get; set; } ICollection<TKey> Keys { get; } ICollection<TValue> Values { get; } void Add(TKey key, TValue value); bool ContainsKey(TKey key); bool Remove(TKey key); bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value); }

Some notable differences:

  • The array index operator is get in the read-only version, and get/set in the writeable version. Would it be possible to specify this if inheriting from IReadOnlyDictionary?
  • The Keys and Values are IEnumerable in the read-only version, and ICollection in the writeable version. This also doesn't make sense to me. When would you add a key to a dictionary without a value, or add a value to a dictionary without a key? I feel like it would make more sense for the writeable dictionary to also return IEnumerable or IReadOnlyCollection for these properties.

The .Net team put a lot of thought into the core classes, so I assume the interfaces are separate by design. Does anyone know why the interfaces were created this way?

3 Answers3

7

Do not confuse "read-only" with "immutable" in this case. With respect to most collections in .NET, the "read-only" moniker is fancy-talk for "cannot add or remove items". Nothing more.

Conceptually and academically, there is nothing wrong with IDictionary deriving from IReadOnlyDictionary. As a thought experiment, replace "Dictionary" with "BlogPostRepository":

public interface IReadonlyBlogRepository
{
    BlogPost Find(int id);
}

public interface IBlogRepository : IReadonlyBlogRepository { void Add(BlogPost post); void Delete(BlogPost post); }

One constrains consumers so they can only query for blog data. The other allows consumers to read and write data. This is a pretty typical breakdown in responsibilities across many domains.

The different interfaces allow your code to express that it needs to modify a collection versus when it does not need to modify the collection.

As for why IDictionary in .NET does not derive from IReadOnlyDictionary? This is a matter of timing. The IDictionary<TKey, TValue> interface was introduced in .NET 2.0. The IReadOnlyDictionary<TKey, TValue> interface was introduced in .NET 4.5.

Had the .NET team modified the original IDictionary interface, not all code that compiled using IDictionary in .NET 4.5 would have been usable in .NET 2. It may have been an issue of backwards compatibility between .NET versions, but this is pure conjecture on my part.

4

Preface: this answer is predicated on a misunderstanding that IReadOnlyDictionary means "A dictionary that is read-only". It actually means "a Dictionary that's at least readable (but perhaps also writable).

From an academic perspective, mutable collections can never (correctly) be subtypes of their immutable counterparts.

While they might seem to satisfy the Liskov substitution principle in that they support all the methods of the immutable containers, there's more to LSP than just a list of methods. To be correct, objects of the subtype must be fully substitutable where objects of the supertype are expected. This isn't possible here, because the callers might rely on the read-only aspect of immutable collections, which mutable collections obviously can't provide.

As a concrete example, imagine a piece of code which takes a read-only dictionary, and provides some aggregate statistics about the values. Authors of this code might choose to cache these stats, so that they're only calculated once, and the same results can be quickly regurgitated on demand. If a Dictionary is allowed to be passed in, this would no longer work, as the aggregate stats can change unbeknownst to this code (which would need to know to invalidate its cache), leading the caches to go stale, and give incorrect results.

You might also be interested why in software, a (mutable) square actually isn't a (mutable) rectangle. Why would Square inheriting from Rectangle be problematic if we override the SetWidth and SetHeight methods?

Alexander
  • 5,185
0

Mutable collection interfaces in C# do not extend their readonly counterparts because:

  1. At the time that C# was initially laid down, the benefits of immutability were not widely understood, and therefore the benefits of readonlyness were also not widely understood. So, the first versions of the language had mutable interfaces but not readonly ones. Instead, the language put a lot of emphasis on getting covariance right, and as we will see, this had certain unfortunate implications later.

  2. Readonly interfaces were introduced a long time after their mutable counterparts. When they were finally introduced, the support for covariance that had already been built into the language made it impossible to change the mutable interfaces to make them extend the readonly ones. Eric Lippert explains this better than I could, on various occasions, for example here: Stack Overflow: C# generic inheritance and covariance part 2

Luckily, it is easy to create a readonly collection interface out of a mutable one. Specifically for the case of IReadOnlyDictionary<K, V>, create a new public static class called DictionaryExtensions, and add the following function in it:

public static IReadOnlyDictionary<K, V> AsReadOnly<K, V>(
    this IDictionary<K, V> self ) where K : notnull
        => new ReadOnlyDictionaryOnDictionary<K, V>( self );

The implementation of class ReadOnlyDictionaryOnDictionary<K, V> is fairly trivial: it accepts an IDictionary<K, V> as a constructor parameter, and it implements each method of IReadOnlyDictionary<K, V> by delegating to the corresponding method in IDictionary<K, V>.

From that moment on, when you want an IReadOnlyDictionary<K, V> from a mutableDictionary you can just say mutableDictionary.AsReadOnly().

The same mechanism can be used for creating:

  • an IReadOnlyCollection<T> from an ICollection<T>
  • an IReadOnlyList<T> from an IList<T>

...etc.

Unfortunately, this mechanism involves the creation of a new object, so it is not as efficient as it would be if we could simply down-cast a mutable interface to its readonly counterpart, but it is as good at it can get.

Mike Nakis
  • 32,803