16

我的问题与这个问题有些相关:Explicitly implementation interface and generic constraint

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

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

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

以及由此产生的 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是。

所以我不知道进行虚拟调用所需的装箱是什么,而且我特别不明白使用限制为值类型的泛型如何消除装箱操作的需要。

我先谢谢大家...

4

5 回答 5

25

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

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

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

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

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

如果这就是调用虚拟方法的方式,那么vtable 来自哪里?值类型中没有 vtable。值类型只是在其存储中具有它的值。装箱会创建一个对对象的引用,该对象的 vtable 设置为指向所有值类型的虚拟方法。(再次提醒您,这并不是接口调用的工作方式,但它是一种很好的思考方式。)

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

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

于 2011-04-03T19:54:20.800 回答
8

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

  • Opcodes.Box,实现装箱转换,将值类型值转为对象。该对象在偏移量 0 处具有方法表指针。
  • Opcodes.Contrained,直接将抖动交给方法表指针,无需装箱。由通用约束启用。

后者显然更有效。

于 2011-04-03T19:56:06.883 回答
5

当值类型对象被传递给期望接收类类型对象的例程时,装箱是必要的。像这样的方法声明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-mutating 结构(例如List<string>.Enumerator),则使用ref将有必要确保this结构在执行期间执行的突变ReadAndAdvanceEnumerator将在调用者的副本上执行。

于 2012-02-06T18:10:34.033 回答
1

通用约束仅提供编译时检查是否将正确的类型传递到方法中。最终结果总是编译器生成一个接受运行时类型的适当方法:

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.
}

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

于 2011-04-03T19:49:40.240 回答
0

我认为你需要使用

  • 反射器
  • ildasm / 单声道

真正得到你想要的答案

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

于 2011-04-03T19:45:24.393 回答