为什么 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
指令进行编译。这是为了防止不必要的装箱。
这是我在所有 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
指令进行编译。这是为了防止不必要的装箱。