通用约束如何防止使用隐式实现的接口装箱值类型?

Mik*_*lum 13 .net c# generics boxing interface

我的问题与此问题有些相关:显式实现的接口和通用约束.

然而,我的问题是编译器如何启用通用约束来消除对显式实现接口的值类型进行装箱的需要.

我想我的问题归结为两部分:

  1. 幕后CLR实现发生了什么,需要在访问显式实现的接口成员时将值类型装箱,并且

  2. 删除此要求的通用约束会发生什么?

一些示例代码:

internal struct TestStruct : IEquatable<TestStruct>
{
    bool IEquatable<TestStruct>.Equals(TestStruct other)
    {
        return true;
    }
}

internal class TesterClass
{
    // Methods
    public static bool AreEqual<T>(T arg1, T arg2) where T: IEquatable<T>
    {
        return arg1.Equals(arg2);
    }

    public static void Run()
    {
        TestStruct t1 = new TestStruct();
        TestStruct t2 = new TestStruct();
        Debug.Assert(((IEquatable<TestStruct>) t1).Equals(t2));
        Debug.Assert(AreEqual<TestStruct>(t1, t2));
    }
}
Run Code Online (Sandbox Code Playgroud)

由此产生的IL:

.class private sequential ansi sealed beforefieldinit TestStruct
    extends [mscorlib]System.ValueType
    implements [mscorlib]System.IEquatable`1<valuetype TestStruct>
{
    .method private hidebysig newslot virtual final instance bool System.IEquatable<TestStruct>.Equals(valuetype TestStruct other) cil managed
    {
        .override [mscorlib]System.IEquatable`1<valuetype TestStruct>::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>(!!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<!!T>::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<valuetype TestStruct>::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<valuetype TestStruct>(!!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 
    }

}
Run Code Online (Sandbox Code Playgroud)

键调用constrained !!T代替box TestStruct,但后续调用仍然callvirt在两种情况下.

所以我不知道进行虚拟调用所需的拳击是什么,我特别不明白如何使用通用约束到值类型来消除对装箱操作的需要.

我提前感谢大家......

Eri*_*ert 22

然而,我的问题是编译器如何启用通用约束来消除对显式实现接口的值类型进行装箱的需要.

通过"编译器",您不清楚是指抖动还是C#编译器.C#编译器通过在虚拟调用上发出约束前缀来实现此目的.有关详细信息,请参阅约束前缀的文档.

幕后CLR实现发生了什么,需要在访问显式实现的接口成员时将值类型装箱

被调用的方法是否是显式实现的接口成员并不是特别相关.一个更普遍的问题是为什么任何虚拟调用都需要将值类型装箱?

传统上认为虚拟调用是对虚函数表中的方法指针的间接调用.这并不完全是接口调用在CLR中的工作方式,但对于本次讨论而言,这是一个合理的心智模型.

如果这是调用虚拟方法的方式,那么vtable来自哪里?值类型中没有vtable.值类型只在其存储中具有其值.Boxing创建对一个对象的引用,该对象的vtable设置为指向所有值类型的虚方法.(我再次提醒你,这是不准确的调用接口是如何工作的,但想想它的好方法.)

删除此要求的通用约束会发生什么?

抖动将为泛型方法的每个不同值类型参数构造生成代码.如果您要为每种不同的值类型生成新代码,那么您可以将该代码定制为该特定值类型.这意味着您不必构建vtable,然后查看vtable的内容是什么!您知道vtable的内容是什么,所以只需生成代码即可直接调用该方法.

  • "一个更普遍的问题是为什么任何虚拟调用都需要将值类型装箱?" - 确实!你的回答一如既往地非常清晰.所以基本上,虚拟调用需要类型信息,值类型内部缺乏.很酷.非常感谢Eric. (5认同)

Han*_*ant 6

最终目标是获取指向类的方法表的指针,以便可以调用正确的方法.这不可能直接在值类型上发生,它只是一个字节的blob.有两种方法可以实现:

  • Opcodes.Box,实现装箱转换并将值类型值转换为对象.该对象的方法表指针位于偏移量0处.
  • Opcodes.Crarained,直接用手抖动方法表指针,无需拳击.由通用约束启用.

后者显然更有效率.

  • +1很好的答案,汉斯,非常感谢. (2认同)

sup*_*cat 5

当将值类型对象传递给期望接收类类型对象的例程时,装箱是必要的。像这样的方法声明string ReadAndAdvanceEnumerator<T>(ref T thing) where T:IEnumerator<String>实际上声明了整个函数系列,每个函数都需要不同的 type T。如果T碰巧是值类型(例如List<String>.Enumerator),即时编译器实际上将专门生成机器代码来执行ReadAndAdvanceEnumerator<List<String>.Enumerator>()ref顺便说一句,请注意;的使用 如果T是类类型(在除约束之外的任何上下文中使用的接口类型都算作类类型),则使用ref将会对效率造成不必要的障碍。但是,如果有可能是T-mutatingthis结构(例如),则有必要List<string>.Enumerator使用来确保在执行 期间由结构执行的突变将在调用者的副本上执行。refthisReadAndAdvanceEnumerator

  • 一个非常受欢迎的补充,超级猫,很好的解释。我也喜欢你在这里对“ref”的使用的解释,这是一个很好的微妙之处。 (2认同)