The Story of Read-Only Collection Interfaces in .NET
.NET 4.5 adds two new collection interfaces, IReadOnlyList and IReadOnlyDictionary. While these interfaces are quite humble on the surface, they expose the rather complex story of backwards compatibility, interoperability, and the role of covariance.
IReadOnlyList and IReadOnlyDictionary are interfaces that .NET developers have wanted since the very beginning. In addition to providing a sense of symmetry, a read-only interface would eliminate the need to implement methods that would do nothing but throw a NotSupportedException. For reasons lost to time, this wasn’t done.
The next opportunity was with the introduction of generics with .NET 2. This allowed Microsoft to phase out the weakly typed collections and interface and replace them with strongly typed counter-parts. The Base Class Library team again passed on the chance to offer a read-only list, with Kit George writing,
Because we could provide a default implementation for what you're talking about Joe, rather than giving you an interface, we provided ReadOnlyCollectionBase. However, given that it wasn't strongly typed, I can understand a reluctance to use it. But with the introduction of generics, we now also have ReadOnlyCollection<T>, so you get the same functionality, but strongly-typed: awesome!
ReadOnlyCollection<T> isn't sealed, so feel free to write your own collection on top if needed. We have no plans to introduce an interface for this same concept, since the collections we've made for this suit the general need.
Krzysztof Cwalina weighed in on the subject as well,
It may sound surprising, or not, but IList and IList<T> are our interfaces intended for read-only collections. They both have IsReadOnly Boolean property that should return true when implemented by a read-only collection. The reason we don’t want to add a purely read-only interface is that we feel it would add too much unnecessary complexity to the library. Note that by complexity, we mean both the new interface and its consumers.
We feel that API designers either don’t care about checking the IsReadOnly property at runtime and potentially throwing an exception, in which case IList is fine, or they would like to provide a really clean custom API, in which case they explicitly implement IList and publicly expose custom tailored read-only API. The latter is typical for collections exposed form object models.
While developers grumbled about the situation, the new opportunities offered by generics far out-weighed this one sticking point and the issue was largely ignored until .NET 4. However, there are repercussions from this decision that we will discuss later on.
With .NET 4 an exciting new capability was added to the runtime. In previous versions of .NET interfaces were overly restrictive when it came to types. For example, one could not use an object of type IEnumerable<Customer> as a parameter to a function expecting IEnumerable<Person> even though the Customer class inherited from Person. With the addition of covariance support, that limitation was partially lifted.
We say “partially” because there are several scenarios where one would like to use an interface with a richer API than IEnumerable. And while IList isn’t covariant, a read-only list interface would be. Unfortunately the .NET BCL team again decided not to address this oversight.
Then the introduction of WinRT and the resurgence of COM changed everything. COM interoperability was once something developers used when no other options were available, it is now a cornerstone of .NET programming. And since WinRT exposes the interfaces IVectorView<T> and IMapView<K, V>, so must .NET.
It is important to note that the C++/WinRT names of the interfaces are somewhat more accurate. These interfaces are intended to represent views into a collection, but they do not ensure the collection itself is immutable. It is a common mistake among even experienced .NET developers to assume that ReadOnlyCollection is an immutable copy of a collection when in fact it is just a wrapper around a live collection. (For more on read-only, frozen, and immutable collections see Andrew Arnott’s post by the same name.)
One may find it interesting to know that IList<T> does not inherit from IReadOnlyList<T>, even though it has all the same members and all lists can be expressed as a read-only list. Immo Landwerth explains,
It looks like a reasonable assumption that it works because the read-only interfaces are purely a subset of the read-write interfaces. Unfortunately, it is incompatible because at the metadata level every method on every interface has its own slot (which makes explicit interface implementations work).
Or in other words, the only opportunity they had to introduce the read-only interfaces as base classes of the mutable variety was back in .NET 2.0 when they were originally conceived. Once released into the wild, the only change that can be made to it is adding the covariant and/or contravariant markers (expressed as “in” and “out” in VB and C#).
When asked why there is no IReadOnlyCollection<T> Immo responded,
We considered this design, but we felt adding a type that only provides a Count property does not add much value to the BCL. In the BCL team we believe that an API start at minus a thousand points and thus providing some value is not good enough to justify being added. The reason that adding new APIs also has cost, for example developers have more concepts to choose from. Initially we thought that adding this type would allow code to gain better perf in scenarios where you just want to get the count and then do some interesting stuff with it. For example, bulk adding to an existing collection. However, for those scenarios we already encourage people to just take an IEnumerable<T> and special case having the instance implementing ICollection<T> too. Since all of our built-in collection types implement this interface there are no perf gains in the most common scenarios. BTW, the Count() extension methods on IEnumerable<T> do this as well.
The new interfaces are avaialble for .NET 4.5 and .NET for Windows 8.
Co-variance and contra-variance was supported in .NET 2.0 and above at the CLR level. The C#/VB compilers did not expose this until .NET 4.0.
Brandon Holt, Preston Briggs, Luis Ceze, Mark Oskin May 21, 2015
Kai Kreuzer, Olaf Weinmann May 21, 2015