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...
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.