Search code examples
c#.netreadonly-collection

Why does List.AsReadOnly return a ReadOnlyCollection but Dictionary.AsReadOnly returns an IReadOnlyDictionary?


I ran into a case where the fact that List.AsReadOnly() returns a ReadOnlyCollection instead of an IReadOnlyCollection made things hard for me. Since the returned collection was the Value of a Dictionary, it could not be automatically upcast to an IReadOnlyCollection. This seemed strange, and upon review of the .Net source code I confirmed that the AsReadOnly() method does something different for List than it does for Dictionary, namely, returning the concrete class instead of the interface.

Can anyone explain why this is? It seems a disservice to have this inconsistency, especially because we want to use the interfaces when possible and especially when public.

In my code, I was thinking at first that since my consumer was just a private method, I could change its parameter signature from IReadOnlyDictionary<T, IReadOnlyCollection<T>> to IReadOnlyDictionary<T, ReadOnlyCollection<T>>. But, then I realized that this makes it look like the private method might modify the collection values, so I put an annoying explicit cast into the earlier code in order to properly use the interface:

.ToDictionary(
   item => item,
   item => (IReadOnlyCollection<T>) relatedItemsSelector(item)
      .ToList()
      .AsReadOnly() // Didn't expect to need the direct cast
)

Oh, and since I'm always getting covariance and contravariance confused, could someone please tell me which one is preventing the automatic cast, and try to remind me in a sensible way how to remember them for the future? (e.g., collections are not ______variant [co/contra] for _____ [input/output] parameters.) I understand why this can't be, because there can be many implementations of the interface and it isn't safe to cast all the individual elements of the dictionary to the requested type. Unless I'm blowing even this simple aspect and I don't understand it, in which case I hope you can help set me right...


Solution

  • The reason is historical. The IReadOnly* interfaces were added in .NET 4.5, while List<T>.AsReadOnly() was added back in .NET 2.0. Changing its return type would've been a breaking change.

    The explicit cast isn't so bad. It's not even a runtime cast, since the compiler can statically verify it (no cast is emitted to IL). By the way, you can cast it to IReadOnlyList<T> which also offers indexed access to the list. You could also write an extension method that returns the type you need (e.g. AsReadOnlyList()).

    Regarding {co,contra}variance, I find it easier to remember using the C# keywords in (contravariant) and out (covariant). in type parameters can only appear as input method arguments, while out type parameters can only appear as output (return values). A method that accepts a parameter, e.g. of type Base, is safe to be called with the type Derived, therefore it's safe to cast in parameters in that direction. out is just the opposite.

    For example:

    interface IIn<in T> { Set(T value); }
    IIn<Base> b = ...
    IIn<Derived> d = b;
    d.Set(derived); // safe since any method accepting Base can handle Derived
    
    interface IOut<out T> { T Get(); }
    IOut<Derived> d = ...
    IOut<Base> b = d;
    b.Get(); // safe since any Derived is Base
    

    Non-read-only collection interfaces can't be *-variant since they'd have to be both in and out, and that would be unsafe. The compiler and the CLR doesn't allow that. .NET does have a form of unsafe variance with arrays:

    var a = new[] { "s" };
    var o = (object[])a;
    o[0] = 1; // ArrayTypeMismatchException
    

    You can see how they wanted to avoid that mess with generic variance. Presumably they could add write-only interfaces that would allow the in (cotravariant) direction, but I'm guessing they didn't find a lot of value in that.