44

In earlier versions of C# IEnumerable was defined like this:

public interface IEnumerable<T> : IEnumerable

Since C# 4 the definition is:

public interface IEnumerable<out T> : IEnumerable
  • Is it just to make the annoying casts in LINQ expressions go away?
  • Won't this introduce the same problems like with string[] <: object[] (broken array variance) in C#?
  • How was the addition of the covariance done from a compatibility point of view? Will earlier code still work on later versions of .NET or is recompilation necessary here? What about the other way around?
  • Was previous code using this interface strictly invariant in all cases or is it possible that certain use cases will behave different now?
Mechanical snail
  • 29,755
  • 14
  • 88
  • 113
soc
  • 27,983
  • 20
  • 111
  • 215

3 Answers3

53

Marc's and CodeInChaos's answers are pretty good, but just to add a few more details:

First off, it sounds like you are interested in learning about the design process we went through to make this feature. If so, then I encourage you to read my lengthy series of articles that I wrote while designing and implementing the feature. Start from the bottom of the page:

Covariance and contravariance blog posts

Is it just to make the annoying casts in LINQ expressions go away?

No, it is not just to avoid Cast<T> expressions, but doing so was one of the motivators that encouraged us to do this feature. We realized that there would be an uptick in the number of "why can't I use a sequence of Giraffes in this method that takes a sequence of Animals?" questions, because LINQ encourages the use of sequence types. We knew that we wanted to add covariance to IEnumerable<T> first.

We actually considered making IEnumerable<T> covariant even in C# 3 but decided that it would be strange to do so without introducing the whole feature for anyone to use.

Won't this introduce the same problems like with string[] <: object[] (broken array variance) in C#?

It does not directly introduce that problem because the compiler only allows variance when it is known to be typesafe. However, it does preserve the broken array variance problem. With covariance, IEnumerable<string[]> is implicitly convertible to IEnumerable<object[]>, so if you have a sequence of string arrays, you can treat that as a sequence of object arrays, and then you have the same problem as before: you can try to put a Giraffe into that string array and get an exception at runtime.

How was the addition of the covariance done from a compatibility point of view?

Carefully.

Will earlier code still work on later versions of .NET or is recompilation necessary here?

Only one way to find out. Try it and see what fails!

It's often a bad idea to try to force code compiled against .NET X to run against .NET Y if X != Y, regardless of changes to the type system.

What about the other way around?

Same answer.

Is it possible that certain use cases will behave different now?

Absolutely. Making an interface covariant where it was invariant before is technically a "breaking change" because it can cause working code to break. For example:

if (x is IEnumerable<Animal>)
    ABC();
else if (x is IEnumerable<Turtle>)
    DEF();

When IE<T> is not covariant, this code chooses either ABC or DEF or neither. When it is covariant, it never chooses DEF anymore.

Or:

class B     { public void M(IEnumerable<Turtle> turtles){} }
class D : B { public void M(IEnumerable<Animal> animals){} }

Before, if you called M on an instance of D with a sequence of turtles as the argument, overload resolution chooses B.M because that is the only applicable method. If IE is covariant, then overload resolution now chooses D.M because both methods are applicable, and an applicable method on a more-derived class always beats an applicable method on a less-derived class, regardless of whether the argument type match is exact or not.

Or:

class Weird : IEnumerable<Turtle>, IEnumerable<Banana> { ... }
class B 
{ 
    public void M(IEnumerable<Banana> bananas) {}
}
class D : B
{
    public void M(IEnumerable<Animal> animals) {}
    public void M(IEnumerable<Fruit> fruits) {}
}

If IE is invariant then a call to d.M(weird) resolves to B.M. If IE suddenly becomes covariant then both methods D.M are applicable, both are better than the method on the base class, and neither is better than the other, so, overload resolution becomes ambiguous and we report an error.

When we decided to make these breaking changes, we were hoping that (1) the situations would be rare, and (2) when situations like this arise, almost always it is because the author of the class is attempting to simulate covariance in a language that doesn't have it. By adding covariance directly, hopefully when the code "breaks" on recompilation, the author can simply remove the crazy gear trying to simulate a feature that now exists.

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • Hey, thanks for taking your time! Yes, I'm pretty much interested in the design process. One thing I'm wondering about is how much impact not supporting covariant return types and "real" (sorry for that) variance in C#/the CLR has on other languages. E. g. there was a post about Scala .NET today. I wonder what they did about those two problems... (<=== Should I open new question for that?) – soc Jul 18 '11 at 15:28
  • @soc: Pretty much any time someone asks a non-trivial question in a comment, it's a good idea to just open up a new question. I am certainly no expert on either Scala or C++/CLI, both of which I believe have return type covariance even though the CLR does not support it natively. – Eric Lippert Jul 18 '11 at 16:55
  • @Eric Lippert: Out of curiosity, would there be any possibility of a future version of vb/c#/.net either allowing a class which implements a read/write property to be implicitly regarded as having read-only and write-only implementations, or else allowing a read-write property to be used in a covariant/contravariant interface by notating it as read-only or write-only? There are many cases where a it would be useful for IList to derive from covariant IReadableList, if it could be done without requiring that implementations of IList add a read-only property. – supercat Jul 18 '11 at 18:00
  • @supercat: Anything is *possible*. We have certainly noticed that we do not have very good support in the type system for reasoning about immutability of class types, and many people have suggested making a "read only list" and "read only array" types that would be more typesafe when used covariantly. However we do not at this time have specific plans for specific features to address those concerns; we just have the knowledge that a lot of people have these concerns. – Eric Lippert Jul 18 '11 at 18:03
  • @Eric Lippert: I wasn't thinking about immutable types, but rather read-only interfaces for existing types (though an IImmutableList which derived from IReadableList would be useful as well). Suppose I want an efficient method to act upon the items in an an IEnumerable in reverse order. It's possible to enumerate to a list, but if the thing is already an IList, that would be inefficient. If I'm expecting an IEnumerable but am given a List, is there any way I can exploit the fact that it's an IList? If IList inherited IReadableList, it would be easy. – supercat Jul 18 '11 at 18:15
28

In order:

Is it just to make the annoying casts in LINQ expressions go away?

It makes things behave like people generally expect ;p

Won't this introduce the same problems like with string[] <: object[] (broken array variance) in C#?

No; since it doesn't expose any Add mechanism or similar (and can't; out and in are enforced at the compiler)

How was the addition of the covariance done from a compatibility point of view?

The CLI already supported it, this merely makes C# (and some of the existing BCL methods) aware of it

Will earlier code still work on later versions of .NET or is recompilation necessary here?

It is entirely backwards compatible, however: C# that relies on C# 4.0 variance won't compile in a C# 2.0 etc compiler

What about the other way around?

That is not unreasonable

Was previous code using this interface strictly invariant in all cases or is it possible that certain use cases will behave different now?

Some BCL calls (IsAssignableFrom) may return differently now

Alex Bagnolini
  • 21,990
  • 3
  • 41
  • 41
Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • So the C# compiler strictly checks variance annotations (unlike arrays) in C#? – soc Jul 18 '11 at 11:43
  • @soc yes; both at the point of *defining* a covariant or contravariant interface / delegate, and at the point of using covariance to *assign* a reference (for example, assign an `IEnumerable` to an `IEnumerable`). If you add `out`, you can **only** have APIs that *return* such; and conversely `in`. As a consequence, you can't have properties / indexers with both a `get` *and* a `set` of that type, `ref` / `out`, etc. And obviously return types and parameter types are checked. – Marc Gravell Jul 18 '11 at 11:45
  • I wonder how that works considering this answer to a slightly related question: http://stackoverflow.com/questions/221691/why-does-ienumerablet-inherit-from-ienumerable/221724#221724 Did anything change since then which allowed MS to change the variance? – soc Jul 18 '11 at 11:46
  • 1
    @soc - nothing at all; note it is only `IEnumerable` that offers this - there is no variance on `IList` or `ICollection`, as they are not compatible; they have both "in" and "out" usage, so are neither covariant nor contravariant. – Marc Gravell Jul 18 '11 at 11:48
  • Ah ok, thanks. But If I had a immutable class I could just reuse the type param, like `class ImmutableList : IEnumerable` (unsure about the C# syntax) instead of adding the `out` to every implemented interface? – soc Jul 18 '11 at 11:52
  • @soc correct; you only use the `out` when ***declaring the interface***, i.e. defining `IEnumerable` - and you aren't declaring that interface. – Marc Gravell Jul 18 '11 at 12:00
  • Mhh. Isn't that the other way around? What I meant is that `class ImmutableList : IEnumerable, IFoo, IBar` is certainly nicer than `class ImmutableList : IEnumerable, IFoo, IBar` – soc Jul 18 '11 at 12:19
  • 1
    @soc in your example, you are *implementing* the interface - you don't have `out` for that - just `: IEnumerable`. Also, `out` doesn't apply to classes - *only* to interfaces and delegates/. – Marc Gravell Jul 18 '11 at 12:41
  • Ouuuuch. You are right. It is not possible specify variance on classes in C#. I forgot that. Well, that's what they deserve for baking the typesystem into the CLR. :-) – soc Jul 18 '11 at 15:24
  • Wait, what? You can't use `out` in classes? *Really!?* So for that case we need to stick with the (essentially equivalent) `where T : MyClass`? Why was this done? – BlueRaja - Danny Pflughoeft Jul 18 '11 at 17:02
  • @Danny Pflughoeft: Making classes covariant or contravariant would have required more complex changes to the CLI, and wasn't felt to be worth the effort. Variance is mainly useful with parameters; in most cases if one wanted Foo to be covariant, one could just as well limit one's use of declared type "Foo" to constructor calls, and everywhere else use IFoo. There are some cases where covariant classes could be useful (e.g. because one can limit substitutability, or because things like exception handling require classes rather than interfaces) but they would make some things trickier. – supercat Jul 18 '11 at 18:07
  • @Marc: I took the liberty to prepend the original questions to your answers to let people enjoy more the "Q-A flow". Feel free to rollback the change if you think it doesn't respect your initial approach to the question (or just hate verbose answers :) ) – Alex Bagnolini Jul 19 '11 at 12:10
9

Is it just to make the annoying casts in LINQ expressions go away?

Not only when using LINQ. It's useful everywhere you have an IEnumerable<Derived> and the code expects a IEnumerable<Base>.

Won't this introduce the same problems like with string[] <: object[] (broken array variance) in C#?

No, because covariance is only allowed on interfaces that return values of that type, but don't accept them. So it's safe.

How was the addition of the covariance done from a compatibility point of view? Will earlier code still work on later versions of .NET or is recompilation necessary here? What about the other way around?

I think already compiled code will mostly work as is. Some runtime type-checks (is, IsAssignableFrom, ...) will return true where they returned false earlier.

Was previous code using this interface strictly invariant in all cases or is it possible that certain use cases will behave different now?

Not sure what you mean by that


The biggest problems are related to overload resolution. Since now additional implicit conversions are possible a different overload might be chosen.

void DoSomething(IEnumerabe<Base> bla);
void DoSomething(object blub);

IEnumerable<Derived> values = ...;
DoSomething(values);

But of course, if these overload behave differently, the API is already badly designed.

CodesInChaos
  • 106,488
  • 23
  • 218
  • 262
  • Ideally, passing an IEnumerable as an IEnumerable behave like passing it as an IEnumerable, but it's not always practical. Unless one uses Reflection, e.g., it would be difficult to write a "Count" method which, given given an ICollection as an IEnumerable, could exploit ICollection.Count. One could argue that ICollection is poorly designed, but I don't think the designers of ICollection had any reason to consider covariance (arguably, List could have implemented IList, but as read-only, but that's water under the bridge). – supercat Jul 19 '11 at 14:49
  • Yes the collection interfaces are poorly designed. There should have been a `IReadOnlyCollection` and a `IReadOnlyList` interface. – CodesInChaos Jul 19 '11 at 18:30
  • Incidentally, when implementing one's own list-ish types, one may safely allow for efficient counting of covariant IEnumerable references to it by implementing a non-generic ICollection which does not allow modification (even if the list itself is not immutable). I wonder whether making List implement non-generic IList and/or ICollection for such a purpose would break anything? – supercat Jul 19 '11 at 18:39