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.
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.