9

The reason this code works is due to the fact that the Enumerator cannot modify the collection:

var roList = new List<string>() { "One", "Two", "Three" };
IEnumerable<object> objEnum = roList;

If we try to do the same thing with a List<object>, instead of IEnumerable<object>, we would get a compiler error telling us that we cannot implicitly convert type List<string> to List<object>. Alright, that one makes sense and it was a good decision by Microsoft, in my opinion, as a lesson learned from arrays.

However, what I can't figure out is why in the world is this hardline rule applicable to something like ReadOnlyCollection? We won't be able to modify the elements in the ReadOnlyCollection, so what is the safety concern that caused Microsoft to prevent us from using covariance with something that's read-only? Is there a possible way of modifying that type of collection that Microsoft was trying to account for, such as through pointers?

B.K.
  • 9,982
  • 10
  • 73
  • 105
  • 1
    "a lesson learned from arrays": regarding array covariance, this was actually a conscious design decision to allow it, because Java had it. It turned out to be a pretty bad idea... http://blogs.msdn.com/b/ericlippert/archive/2007/10/17/covariance-and-contravariance-in-c-part-two-array-covariance.aspx – Thomas Levesque Aug 29 '14 at 20:44
  • @ThomasLevesque Good read, thank you. – B.K. Aug 29 '14 at 20:48

1 Answers1

10

Variance and contravariance only work on interfaces and delegates, not on classes.

However, you can do that (using .NET 4.5):

ReadOnlyCollection<string> roList = ...
IReadOnlyCollection<object> objects = roList;

(because IReadOnlyCollection<T> is covariant)


To answer your question about why variance isn't allowed on classes, here's Jon Skeet's explanation from his C# in Depth book (second edition, §13.3.5, page 394).

NO VARIANCE FOR TYPE PARAMETERS IN CLASSES

Only interfaces and delegates can have variant type parameters. Even if you have a class that only uses the type parameter for input (or only uses it for output), you can’t specify the in or out modifiers. For example Comparer<T>, the common implementation of IComparer<T>, is invariant — there’s no conversion from Comparer<IShape> to Comparer<Circle>.

Aside from any implementation difficulties that this might’ve incurred, I’d say it makes a certain amount of sense conceptually. Interfaces represent a way of looking at an object from a particular perspective, whereas classes are more rooted in the object’s actual type. This argument is weakened somewhat by inheritance letting you treat an object as an instance of any of the classes in it s inheritance hierarchy, admittedly. Either way, the CLR doesn’t allow it.

Thomas Levesque
  • 286,951
  • 70
  • 623
  • 758
  • You're right. I tested it and it works with the read-only interfaces `IReadOnlyCollection` and `IReadOnlyList`. Any particular reason why it does not work with classes? – B.K. Aug 29 '14 at 20:43
  • @B.K., I don't know, but my guess is that it's related some CLR internal implementation details... – Thomas Levesque Aug 29 '14 at 20:45
  • It's probably easier to implement support for interfaces. When you cast from one concrete type to a base concrete type, now the base type has to decide whether to call its methods or the base types which depends on virtual/override/hiding scenarios which are only defined in real inheritance. A ReadOnlyCollection does not inherit from ReadOnlycollection so it is difficult to define what happens. Interfaces always pass calls to the concrete type and there's no ambiguity about whether base or derived is called. It's much clearer what it means to access a covariant interface. IMO – AaronLS Aug 29 '14 at 20:54
  • @AaronLS, that's probably the correct explanation. Glad you posted it, because Google isn't being very helpful about that... – Thomas Levesque Aug 29 '14 at 20:56
  • @AaronLS That is an excellent explanation that makes perfect sense. Thank you so very much. – B.K. Aug 29 '14 at 20:58
  • @B.K., I found an explanation in the book *C# in Depth*; I updated my answer to quote it. – Thomas Levesque Aug 29 '14 at 21:09
  • Thomas, thank you, that's perfect. Go figure that the answer comes from the C# god, Jon Skeet, lol – B.K. Aug 29 '14 at 21:15
  • Still, Scala has co- and contravariance in classes as well, so it can be implemented sanely at least in theory. Just an indirect indication that implementation difficulties play significant role here. Cost/benefit analysis in action :) Though it would be nice to have it across class hierarchies as well, I must admit. – Ivan Danilov Aug 30 '14 at 00:35
  • @IvanDanilov, yes, but Scala runs on the JVM, which doesn't really supports generics at runtime (Java uses type erasure, I assume it's the same for Scala). So at runtime, the type parameter is just `Object`, and `Foo` is the same as `Foo` – Thomas Levesque Aug 30 '14 at 00:39
  • @ThomasLevesque Yeah, I know about type erasure vs type specialization. But that is a detail of implementation, isn't it? I wanted to give an example of sensible language where classes have variance enabled as well. – Ivan Danilov Aug 30 '14 at 01:58
  • @IvanDanilov, yes, it's an implementation detail, but it has strong implications on what can or can't be done with the type system... – Thomas Levesque Aug 30 '14 at 02:03