Search code examples
c#genericsinterfacecovariancevariance

Why doesn't C# covariance work properly with interface type constraints?


Here is a code snippet that I have tested on dotnet fiddle on all 3 available compilers:

using System;

interface Foo<out T1>{ }
interface Baz<out T1>{ }

public class Program
{
    abstract class A {}
    class B : A {}
    
    class Bar<T> : Foo<Baz<T>>{}
    
    static void takeConcrete(Foo<Baz<A>> arg) {}
    
    static void take<T>(Foo<Baz<T>> arg) where T : A 
    {
        takeConcrete(arg);
    }
    
    public static void Main()
    {
        take(new Bar<B>());
    }
}

Now, this compiles flawlessly, as expected. But when I change the definition of A to be interface A {}, then it stops compiling:

using System;

interface Foo<out T1>{ }
interface Baz<out T1>{ }

public class Program
{
    interface A {} // <-- change here
    class B : A {}
    
    class Bar<T> : Foo<Baz<T>>{}
    
    static void takeConcrete(Foo<Baz<A>> arg) {}
    
    static void take<T>(
        Foo<Baz<T>> arg
    ) where T : A {
        takeConcrete(arg); // <-- compiler error here
    }
    
    public static void Main()
    {
        take(new Bar<B>());
    }
}

The compiler error is the following:

Argument 1: cannot convert from Foo<Baz<T>> to <Foo<Baz<Program.A>>

From this question there seems to be a missing : class constraint. And indeed, if I constrain T in the take method to where T : class, A, then it compiles again.

Still, I am wondering: why is this class constraint necessary? As far as I understand C#, interface references are boxed on the heap no matter what, so it really shouldn't matter. If the implementing type happens to be a struct, then there will be only one valid type, but that sounds like something the JIT compiler should be able to figure out.


Solution

  • If T is a struct, then generic type replacement means that the actual (or constructed) type of T is the struct, it is not a boxed interface representation of it.

    Therefore covariance cannot be used if T is not constrained to be a class, because an unboxed struct does not inherit from anything (even from System.Object), and it does not have any representation that can be used like a reference type.


    Let us suppose that T is a struct MyStruct. Your code now becomes:

        static void take(Foo<Baz<MyStruct>> arg) {
            takeConcrete(arg);
        }
    

    But the code for takeConcrete is defined:

        static void takeConcrete(Foo<Baz<A>> arg) {}    
    

    In other words, it expects a boxed A representation, and MyStruct in this context is not boxed.


    Proving the point, all code that uses a generic type that is constrained to an interface but not class will compile with the constrained.callvirt instruction in IL. This is to prevent unnecessary boxing.