38

In the following piece of code I expected to be able to implicitly cast from elements to baseElements because TBase is implicitly convertible to IBase.

public interface IBase { }
public interface IDerived : IBase { }
public class VarianceBug
{
    public void Foo<TBase>() where TBase : IBase
    {
        IEnumerable<TBase> elements = null;
        IEnumerable<IDerived> derivedElements = null;
        IEnumerable<IBase> baseElements;

        // works fine
        baseElements = derivedElements;

        // error CS0266: Cannot implicitly convert type 
        //   'System.Collections.Generic.IEnumerable<TBase>' to 
        //   'System.Collections.Generic.IEnumerable<IBase>'. 
        //   An explicit conversion exists (are you missing a cast?)
        baseElements = elements;
    }
}

However, I get the error that is mentioned in the comment.

Quoting from the spec:

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

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

  • Xi is invariant and an identity conversion exists from Ai to Bi

Checking my code, it appears to be consistent with the spec:

  • IEnumerable<out T> is an interface type

  • IEnumerable<out T> is declared with variant type parameters

  • T is covariant

  • an implicit reference conversion exists from TBase to IBase

So - is it a bug in the C# 4 compiler?

Community
  • 1
  • 1
Omer Mor
  • 5,216
  • 2
  • 34
  • 39
  • What happens when you do cast explicitly? The compiler says that there is one. Since you are downcasting it kinda makes sense..? – flq May 06 '10 at 18:11
  • 2
    Just to make it explicit - it is your last bullet "an implicit reference conversion exists from TBase to IBase" that is untrue (unless you add `: class`). It may be assignable, but it is *not* necessarily a reference-conversion. Without the `: class` it is a "constrained" conversion, which is some magic that lets the same IL call methods (including property accessors) on reference-types and value-types in the same way: http://msdn.microsoft.com/en-us/library/system.reflection.emit.opcodes.constrained.aspx – Marc Gravell May 06 '10 at 18:26
  • Charles: you're wrong - the first assignment works (Works on My Machine (TM)). – Omer Mor May 06 '10 at 19:43
  • Mark: right - my bad. It is not a reference type, so there's no reference conversion. However I'm left with the question of why does the first assignment works? – Omer Mor May 06 '10 at 19:45

2 Answers2

52

Variance only works for reference-types (or there is an identity conversion). It is not known that TBase is reference type, unless you add : class:

 public void Foo<TBase>() where TBase : class, IBase

since I could write a:

public struct Evil : IBase {}
Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • Good answer - adding class constraint works. However - this raises another question: why does the first assignment works? – Omer Mor May 06 '10 at 19:44
  • 3
    @Omer - because `IBase` and `IDerived` *are* treated as references; it is only `TBase` that is undecided. – Marc Gravell May 06 '10 at 19:50
  • @MarcGravell: To be more precise, while interfaces may be implemented by non-heap (value) types, a storage location of an interface type will *always* hold a heap object reference. For example, a `List>` holds references to heap objects implementing `IEnumerator`, while a `List where T:IEnumerator` would, if `T` is `List.Enumerator`, holds instances of that struct type instead. Note that if one tried to store a `List.Enumerator` in a `List>`, the system would copy its fields to a new heap object and store a reference to that. – supercat Aug 10 '12 at 20:10
  • In other situations that's exactly the great thing about generics: That if a type parameter `T` is a value type, then we do not get boxing. For example if `T` happens to be `Int32`, a `List` is not a list of boxed integers. However with interfaces we have boxing, for example `var li = new List { 2, 4, 6, };` gives a list of boxes. – Jeppe Stig Nielsen May 08 '13 at 21:20
14

Marc is correct - I was just about to paste the same response.

See the Covariance & Contravariance FAQ:

http://blogs.msdn.com/csharpfaq/archive/2010/02/16/covariance-and-contravariance-faq.aspx

From the FAQ:

"Variance is supported only if a type parameter is a reference type."

Variance is not supported for value types

The following doesn’t compile either:

// int is a value type, so the code doesn't compile.
IEnumerable<Object> objects = new List<int>(); // Compiler error here.
Bittercoder
  • 11,753
  • 10
  • 58
  • 76
  • And more in line with the question, the following won't work either: `IEnumerable comparables = new List();` – Omer Mor Jul 07 '14 at 12:57