Is this a covariance bug in C# 4?
Asked Answered
D

2

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?

Drumm answered 6/5, 2010 at 17:49 Comment(4)
What happens when you do cast explicitly? The compiler says that there is one. Since you are downcasting it kinda makes sense..?Benzoic
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: msdn.microsoft.com/en-us/library/…Zhao
Charles: you're wrong - the first assignment works (Works on My Machine (TM)).Drumm
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?Drumm
Z
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 {}
Zhao answered 6/5, 2010 at 18:16 Comment(4)
Good answer - adding class constraint works. However - this raises another question: why does the first assignment works?Drumm
@Omer - because IBase and IDerived are treated as references; it is only TBase that is undecided.Zhao
@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<IEnumerator<int>> holds references to heap objects implementing IEnumerator<int>, while a List<T> where T:IEnumerator<int> would, if T is List<int>.Enumerator, holds instances of that struct type instead. Note that if one tried to store a List<int>.Enumerator in a List<IEnumerator<int>>, the system would copy its fields to a new heap object and store a reference to that.Nausea
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<T> is not a list of boxed integers. However with interfaces we have boxing, for example var li = new List<IFormattable> { 2, 4, 6, }; gives a list of boxes.Xenia
P
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.
Pocked answered 6/5, 2010 at 18:20 Comment(1)
And more in line with the question, the following won't work either: IEnumerable<IComparable> comparables = new List<int>();Drumm

© 2022 - 2024 — McMap. All rights reserved.