通用约束如何防止使用隐式实现的接口装箱值类型?
我的问题与此问题有些相关: 显式实现的接口和通用约束 。
然而,我的问题是编译器如何启用通用约束来消除对显式实现接口的值类型进行装箱的需要。
我想我的问题归结为两个部分:
-
在访问显式实现的接口成员时需要将值类型装箱的幕后CLR实现发生了什么,以及
-
删除此要求的通用约束会发生什么?
一些示例代码:
internal struct TestStruct : IEquatable { bool IEquatable.Equals(TestStruct other) { return true; } } internal class TesterClass { // Methods public static bool AreEqual(T arg1, T arg2) where T: IEquatable { return arg1.Equals(arg2); } public static void Run() { TestStruct t1 = new TestStruct(); TestStruct t2 = new TestStruct(); Debug.Assert(((IEquatable) t1).Equals(t2)); Debug.Assert(AreEqual(t1, t2)); } }
由此产生的IL:
.class private sequential ansi sealed beforefieldinit TestStruct extends [mscorlib]System.ValueType implements [mscorlib]System.IEquatable`1 { .method private hidebysig newslot virtual final instance bool System.IEquatable.Equals(valuetype TestStruct other) cil managed { .override [mscorlib]System.IEquatable`1::Equals .maxstack 1 .locals init ( [0] bool CS$1$0000) L_0000: nop L_0001: ldc.i4.1 L_0002: stloc.0 L_0003: br.s L_0005 L_0005: ldloc.0 L_0006: ret } } .class private auto ansi beforefieldinit TesterClass extends [mscorlib]System.Object { .method public hidebysig specialname rtspecialname instance void .ctor() cil managed { .maxstack 8 L_0000: ldarg.0 L_0001: call instance void [mscorlib]System.Object::.ctor() L_0006: ret } .method public hidebysig static bool AreEqual<([mscorlib]System.IEquatable`1) T>(!!T arg1, !!T arg2) cil managed { .maxstack 2 .locals init ( [0] bool CS$1$0000) L_0000: nop L_0001: ldarga.s arg1 L_0003: ldarg.1 L_0004: constrained !!T L_000a: callvirt instance bool [mscorlib]System.IEquatable`1::Equals(!0) L_000f: stloc.0 L_0010: br.s L_0012 L_0012: ldloc.0 L_0013: ret } .method public hidebysig static void Run() cil managed { .maxstack 2 .locals init ( [0] valuetype TestStruct t1, [1] valuetype TestStruct t2, [2] bool areEqual) L_0000: nop L_0001: ldloca.s t1 L_0003: initobj TestStruct L_0009: ldloca.s t2 L_000b: initobj TestStruct L_0011: ldloc.0 L_0012: box TestStruct L_0017: ldloc.1 L_0018: callvirt instance bool [mscorlib]System.IEquatable`1::Equals(!0) L_001d: stloc.2 L_001e: ldloc.2 L_001f: call void [System]System.Diagnostics.Debug::Assert(bool) L_0024: nop L_0025: ldloc.0 L_0026: ldloc.1 L_0027: call bool TesterClass::AreEqual(!!0, !!0) L_002c: stloc.2 L_002d: ldloc.2 L_002e: call void [System]System.Diagnostics.Debug::Assert(bool) L_0033: nop L_0034: ret } }
键调用是constrained !!T
而不是box TestStruct
,但后续调用在两种情况下仍然是callvirt
。
所以我不知道进行虚拟调用所需的拳击是什么,我特别不明白如何使用通用约束到值类型来消除对装箱操作的需要。
我提前感谢大家……
然而,我的问题是编译器如何启用通用约束来消除对显式实现接口的值类型进行装箱的需要。
通过“编译器”,您不清楚是指抖动还是C#编译器。 C#编译器通过在虚拟调用上发出约束前缀来实现此目的。 有关详细信息,请参阅受约束前缀的文档 。
在幕后的CLR实现中发生了什么,该实现需要在访问显式实现的接口成员时将值类型装箱
被调用的方法是否是显式实现的接口成员并不是特别相关。 一个更普遍的问题是为什么任何虚拟调用都需要将值类型装箱?
传统上认为虚拟调用是虚拟函数表中方法指针的间接调用。 这并不完全是接口调用在CLR中的工作方式,但对于本次讨论而言,这是一个合理的心智模型。
如果这是调用虚拟方法的方式,那么vtable来自哪里? 值类型中没有vtable。 值类型只在其存储中具有其值。 Boxing创建对一个对象的引用,该对象的vtable设置为指向所有值类型的虚方法。 (再一次,我告诫你,这并不是接口调用的工作方式,但这是一个考虑它的好方法。)
删除此要求的通用约束会发生什么?
抖动将为generics方法的每个不同值类型参数构造生成新代码。 如果您要为每个不同的值类型生成新代码,那么您可以将该代码定制为该特定值类型。 这意味着您不必构建vtable,然后查找vtable的内容是什么! 你知道vtable的内容是什么,所以只需生成代码直接调用方法。
最终目标是获取指向类的方法表的指针,以便可以调用正确的方法。 这不可能直接在值类型上发生,它只是一个字节的blob。 有两种方法可以达到目的:
- Opcodes.Box,实现装箱转换并将值类型值转换为对象。 该对象的方法表指针位于偏移量0处。
- Opcodes.Crarained,直接用手抖动方法表指针而不需要装箱。 由通用约束启用。
后者显然更有效率。
将值类型对象传递给期望接收类类型对象的例程时,必须使用Boxing。 一个方法声明,如string ReadAndAdvanceEnumerator
实际上声明了一整套函数,每个函数都需要不同的类型T
如果T
恰好是一个值类型(例如List
),那么Just-In-Time编译器将实际生成机器代码以执行ReadAndAdvanceEnumerator
。 BTW,注意使用.Enumerator>()
ref
; 如果T
是一个类类型( 除了约束之外的任何上下文中使用的接口类型都算作类类型),使用ref
将是效率的不必要的障碍。 但是,如果T
可能是this
-mutating结构(例如List
),则必须使用ref
来确保在执行ReadAndAdvanceEnumerator
期间由结构执行的this
突变将被执行在来电者的副本上。
我想你需要用
- reflection器
- ildasm / monodis
真正得到你想要的答案
您当然可以查看CLR(ECMA)和/或C#编译器( 单声道 )源的规范
generics约束仅提供编译时检查,表明正确的类型正在传递给方法。 最终结果始终是编译器生成一个接受运行时类型的适当方法:
public struct Foo : IFoo { } public void DoSomething(TFoo foo) where TFoo : IFoo { // No boxing will occur here because the compiler has generated a // statically typed DoSomething(Foo foo) method. }
从这个意义上说,它绕过了对值类型装箱的需要,因为创建了一个直接接受该值类型的显式方法实例。
而当将值类型强制转换为已实现的接口时,该实例是一个引用类型,它位于堆上。 因为我们在这个意义上没有利用generics,所以如果运行时类型是值类型,我们会强制转换为接口(以及后续装箱)。
public void DoSomething(IFoo foo) { // Boxing occurs here as Foo is cast to a reference type of IFoo. }
删除generics约束只会停止编译时检查是否将正确的类型传递给方法。