3

Why does C# implicitly convert generic types parameterized with a reference type implementing an interface to the same generic type parameterized with the implemented interface, but not perform the same implicit conversion for reference types?

Essentially, why does the first line compile but the second one fail?

IEnumerable<IComparable<Version>> x = Enumerable.Empty<Version>();
IEnumerable<IComparable<int>> y = Enumerable.Empty<int>();

Especially great would be a reference to the part of the spec that describes this behavior.

Rob
  • 26,989
  • 16
  • 82
  • 98
Greg Bell
  • 33
  • 1
  • 5
  • 1
    The second would require boxing. – juharr Dec 15 '16 at 19:00
  • True, but the standard says that boxing conversions are implicit, just like converting a class reference to an interface reference. (§6.1.6-7) – Greg Bell Dec 15 '16 at 19:10
  • I'm not big on reading the spec but I'm guessing that has to do with direct convertions, where as this is a co-variant convertion. Basically the first one is assigning a sequence of references to a sequence of references where as the second is attempting to assign a sequence of values to a sequence of references, meaning it needs to box each potential value in the sequence for that to work. – juharr Dec 15 '16 at 19:22
  • @juharr - I figured that too, but I was hoping to find a more detailed explanation of when value types are implicitly boxed and when they are not. I couldn't find it in the spec or with googling, but of course that's no guarantee its not out there. – Greg Bell Dec 15 '16 at 19:33
  • Hopefully one of the guys on here that know the spec will be able to point you to the exact answer you're looking for. – juharr Dec 15 '16 at 19:39
  • 1
    Voting to reopen because, while the linked question certainly answers one part, none of the answers there attempt to give "the part of the spec that describes this behavior" (nor would that be appropriate there). It isn't completely trivial to see that the root cause is the same in both cases. – Jeroen Mostert Dec 16 '16 at 10:51

1 Answers1

3

Short answer

Despite the name "implicit", implicit conversions don't apply unless the rules explicitly say they do, and the rules don't allow boxing conversions when going from IEnumerable<int> to IEnumerable<IComparable<int>>. As a simpler case, you can't go from IEnumerable<int> to IEnumerable<object> for the same reason, and that case is well documented.

Long answer

OK, first of all, why would IEnumerable<T> convert to IEnumerable<IComparable<T>> at all? This is covered in §6.1.6 (C# Language Specification 5.0):

The implicit reference conversions are:

[...]

  • From any reference-type to an interface or delegate type T if it has an implicit identity or reference conversion to an interface or delegate type T0 and T0 is variance-convertible (§13.1.3.2) to T.

And §13.1.3.2 says:

A type T<A1, …, An> is variance-convertible to a type T<B1, …, Bn> if T is either an interface or a delegate type declared with the variant type parameters T<X1, …, Xn>, and for each variant type parameter Xi one of the following holds:

  • Xi is covariant and an implicit reference or identity conversion exists from Ai to Bi

Since IEnumerable<T> is covariant in T, this means that if there is an implicit reference or identity conversion from T to IComparable<T>, then there is an implicit reference conversion from IEnumerable<T> to IEnumerable<IComparable<T>>, by virtue of these being variance-convertible.

I emphasized "reference" for a reason, of course. Since Version implements IComparable<Version>, there is an implicit reference conversion:

  • From any class-type S to any interface-type T, provided S implements T.

Right, so now, why doesn't IEnumerable<int> implicitly convert to IEnumerable<IComparable<int>>? After all, int implicitly converts to IComparable<int>:

IComparable<int> x = 0;  // sure

But it does so not through a reference conversion or an identity conversion, but through a boxing conversion (§6.1.7):

A boxing conversion exists from any non-nullable-value-type [...] to any interface-type implemented by the non-nullable-value-type.

The rules of §13.1.3.2 do not allow boxing conversions in considering whether a variance conversion is possible, and there is no other rule that would enable an implicit conversion from IEnumerable<int> to IEnumerable<IComparable<int>>. Despite the name, implicit conversions are covered by explicit rules.

There is actually a much simpler illustration of this problem:

object x = 0;  // sure, an int is an object
IEnumerable<object> x = new int[] { 0 };  // except when it's not

This isn't allowed for the same reason: there is no reference conversion from int to object, only a boxing conversion, and those are not considered. And in this form, there are several questions on Stack Overflow that explain why this is not allowed (like this one). To summarize: it's not that this is impossible, but to support it, the compiler would have to generate supporting code to stick the code for the boxing conversion somewhere. The C# team valued transparency in this case over ease of use and decided to allow identity-preserving conversions only.

Finally, as a matter of practical consideration, suppose you had an IEnumerable<int> and you needed an IEnumerable<IComparable<int>>, how would you get it? Well, by doing the boxing yourself:

Func<int, IComparable<int>> asComparable = i => i;  // compiles to ldarg ; box ; ret
IEnumerable<IComparable<int>> x = Enumerable.Empty<int>().Select(asComparable);

Of course using Enumerable.Cast would be more practical here; I wrote it this way to highlight that an implicit conversion is involved. There is a cost to this operation, and that's just the point; the C# designers wanted this cost to be explicit.

Community
  • 1
  • 1
Jeroen Mostert
  • 27,176
  • 2
  • 52
  • 85