为什么 C# 协变不能与接口类型约束一起正常工作?

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

这是我在所有 3 个可用编译器的 dotnet fiddle 上测试过的代码片段:

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>());
    }
}

现在,正如预期的那样,编译完美无缺。但是当我将 A 的定义更改为 interface A {} 时,它停止编译:

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>());
    }
}

编译错误如下:

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

this question 开始,似乎缺少 : class 约束。事实上,如果我将 take 方法中的 T 约束为 where T : class, A,那么它会再次编译。

不过,我想知道:为什么这个 class 约束是必要的?就我对 C# 的理解而言,接口引用无论如何都被装箱在堆中,所以它真的不重要。如果实现类型恰好是一个结构,那么将只有一种有效类型,但这听起来像是 JIT 编译器应该能够弄清楚的东西。

如果 T 是结构,那么泛型类型替换意味着 实际(或构造)类型 T 结构,不是它的盒装接口表示。

因此,如果 T 未被限制为 class,则不能使用协方差,因为未装箱的结构不会继承任何东西(即使是 System.Object),它确实没有任何可以像引用类型一样使用的表示。


让我们假设 T 是一个结构 MyStruct 您的代码现在变为:

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

但是takeConcrete的代码定义为:

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

换句话说,它需要一个 boxed A 表示,并且 MyStruct 在这种情况下没有装箱。


证明这一点, 所有使用泛型类型的代码都受限于一个接口,但 不是 class 将使用 IL 中的 constrained.callvirt 指令进行编译。这是为了防止不必要的装箱。