1

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?

6
  • I’m voting to close this question because this is not the place to discuss design decisions made by the .NET team.
    – Rik D
    Commented Jul 10, 2023 at 21:50
  • 3
    If a IDictionary was also a IReadOnlyDictionary, then I could have a IDictionary, pass it to some class as a IReadOnlyDictionary and suppose that class relies on it's read-only-ness, now I take my IDictionary reference and write to it and upset my class. The 'ugly' code you had to write is a good thing, you were forced to make a new dictionary and so mutating the original one doesn't mutate your read only one. Commented Jul 10, 2023 at 22:00
  • "Why does IDictionary not implement IReadOnlyDictionary" – Because it is writeable and not read-only? Commented Jul 11, 2023 at 1:19
  • I think it becomes more obvious if you were to call out the writability in the name with the same prominence as the read-only-ness. "Should IWriteableDictionary be a subtype of IReadOnlyDictionary? No, clearly not :)
    – Alexander
    Commented Jul 11, 2023 at 1:38
  • @JustSomeGuy Dictionary is IReadOnlyDictionary, yet I can mutate it and do exactly what you are saying shouldn't be possible. The interface allows you to guarantee that consumers will not modify the dictionary, but it doesn't guarantee the dictionary is immutable - that comes from the 'IsReadOnly' property Commented Jul 11, 2023 at 2:29

2 Answers 2

6

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
  • I'm trying to think of an example of code that would break if a core interface were updated to implement a new interface in a later version. I can't think of any static examples, but perhaps code using reflection which accesses the interface implementations by a constant index, instead of searching with a predicate? That would be a rare case and badly implemented code, but I guess the .Net team have to be pretty confident the changes they make won't have any adverse effects at all. Is this still a problem in .Net Core, or is backwards compatibility less of an issue with side-by-side versions? Commented Jul 11, 2023 at 20:59
  • @AndrewWilliamson: Microsoft has pretty stringent requirements around modifying the .NET framework. The "why did they do this" part of the question is unanswerable, unless you work for Microsoft designing the v2.0 through v4.5 versions of the framework. I honestly would not be surprise if this question were closed as being opinion-based, because only Microsoft can provide the answer. Instead, I tried tackling the answerable aspect of the question: could IDictionary derive from IReadOnlyDictionary and still be sound OO design? Yes. Commented Jul 11, 2023 at 21:05
  • By chance, I happened to find an issue on GitHub referring to this exact problem: github.com/dotnet/runtime/issues/31001 Commented Aug 1, 2023 at 10:57
  • Ok. That confirms my suspicions, @AndrewWilliamson. I wondered if it would be a breaking change somehow. See this comment for why: I github.com/dotnet/runtime/issues/31001#issuecomment-536230844 Commented Aug 1, 2023 at 11:21
3

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?

12
  • Ah, that makes sense. I misinterpreted 'IReadOnlyDictionary' as a 'read-indexable' type, rather than as a contract specifying immutability. I've just had a look at 'ICollection' and 'IReadOnlyCollection' and see that they are disjoint as well, presumably for the same reason Commented Jul 11, 2023 at 2:17
  • 1
    I've had a bit more thought about this - see my last comment on the original question. Everyone seems to assume that IReadOnlyXX is a contract that implies the object implementing it is immutable. That's not the case for Dictionary and List, they implement the 'IReadOnlyXX' interfaces but have a separate IsReadOnly property to indicate immutability. There's a lot of overlap between immutable and read-only, but they aren't the same thing so perhaps you can see where the confusion comes from Commented Jul 11, 2023 at 2:37
  • 1
    A relevant consideration here is that this is only a problem because of pass-by-reference. If a dictionary was pass-by-value, then when I pass something into a consumer that expects a read-only dictionary, any future changes I make to my dictionary would not have an effect on the consumer. The consumer would have its own readonly dictionary that would not be subject to change from outside influences.
    – Flater
    Commented Jul 11, 2023 at 2:48
  • 1
    LSP does not apply here. This is not about inheritance, it is about partial implementation. The read-only part of an implementation is just that, there is no academic objection for any read/write collection to have interfaces defined for read and write parts separately. The interfaces do not impact the object in any way thus cannot violate anything in regard to the object. So although being valid statements I don't think this answer is helpful in regard to the question. Commented Jul 11, 2023 at 7:47
  • @MartinMaat Hmmm I don't quite understand your perspective. LSP isn't narrowly about inheritance, it generalizes to "subtyping" of any form (including implementing interfaces). However, I did learn that my answer is based on an incorrect premise. "IReadOnlyDictionary" means "a dictionary that guarantees at least the ability to be read" not "an immutable dictionary that can only be read". Still, if the interface was IImmutableDictionary, all my remarks about LSP would be correct, I believe.
    – Alexander
    Commented Jul 11, 2023 at 13:28

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.