Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Implicit convertion of generic types parameterized with reference types vs value types

Tags:

c#

generics

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.

like image 378
Greg Bell Avatar asked Jan 25 '26 17:01

Greg Bell


1 Answers

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.

like image 154
Jeroen Mostert Avatar answered Jan 28 '26 05:01

Jeroen Mostert



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!