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

时间:2011-04-03 19:36:08

标签: c# .net generics interface boxing

我的问题与此问题有些相关:Explicitly implemented interface and generic constraint

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

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

  1. 在访问显式实现的接口成员时需要将值类型装箱的幕后CLR实现正在进行什么,

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

  3. 一些示例代码:

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

    由此产生的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 
        }
    
    }
    

    关键呼叫是constrained !!T而不是box TestStruct,但在这两种情况下,后续呼叫仍为callvirt

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

    我提前感谢所有人......

5 个答案:

答案 0 :(得分:22)

  

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

“编译器”不清楚你是指抖动还是C#编译器。 C#编译器通过在虚拟调用上发出约束前缀来实现此目的。有关详细信息,请参阅the documentation of the constrained prefix

  

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

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

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

如果这是调用虚拟方法的方式,那么 vtable来自哪里?值类型中没有vtable。值类型只在其存储中具有其值。 Boxing创建对一个对象的引用,该对象的vtable设置为指向所有值类型的虚方法。 (同样,我告诫你,这不是完全接口调用的工作原理,但这是一个考虑它的好方法。)

  

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

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

答案 1 :(得分:6)

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

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

后者显然更有效率。

答案 2 :(得分:3)

将值类型对象传递给期望接收类类型对象的例程时,必须进行装箱。像string ReadAndAdvanceEnumerator<T>(ref T thing) where T:IEnumerator<String>这样的方法声明实际上声明了一整套函数,每个函数都需要一个不同的类型T。如果T碰巧是值类型(例如List<String>.Enumerator),那么Just-In-Time编译器将实际生成机器代码以执行ReadAndAdvanceEnumerator<List<String>.Enumerator>()。顺便说一句,请注意使用ref;如果T是类类型(在任何上下文中使用的接口类型而不是约束计为类类型),ref的使用将是效率的不必要的障碍。但是,如果T可能是this - 变异结构(例如List<string>.Enumerator),则必须使用ref来确保this在执行ReadAndAdvanceEnumerator期间由结构执行的突变将在调用者的副本上执行。

答案 3 :(得分:0)

我认为你需要使用

  • 反射器
  • ildasm / monodis

真正得到你想要的答案

您当然可以查看CLR(ECMA)的规范和/或C#编译器的来源(mono

答案 4 :(得分:0)

泛型约束仅提供编译时检查,表明正确的类型正在传递给方法。最终结果始终是编译器生成一个接受运行时类型的适当方法:

public struct Foo : IFoo { }

public void DoSomething<TFoo>(TFoo foo) where TFoo : IFoo
{
  // No boxing will occur here because the compiler has generated a
  // statically typed DoSomething(Foo foo) method.
}

从这个意义上说,它绕过了对值类型装箱的需要,因为创建了一个直接接受该值类型的显式方法实例。

当将值类型强制转换为已实现的接口时,该实例是一个引用类型,它位于堆上。因为我们在这个意义上没有利用泛型,所以如果运行时类型是值类型,我们会强制转换为接口(以及后续装箱)。

public void DoSomething(IFoo foo)
{
  // Boxing occurs here as Foo is cast to a reference type of IFoo.
}

删除泛型约束只会停止编译时检查是否将正确的类型传递给方法。