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

时间:2022-09-25 12:15:18

My question is somewhat related to this one: Explicitly implemented interface and generic constraint.

我的问题与这个问题有关:显式实现的接口和通用约束。

My question, however, is how the compiler enables a generic constraint to eliminate the need for boxing a value type that explicitly implements an interface.

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

I guess my question boils down to two parts:

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

  1. What is going on with the behind-the-scenes CLR implementation that requires a value type to be boxed when accessing an explicitly implemented interface member, and

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

  2. What happens with a generic constraint that removes this requirement?

    如果通用约束删除了这个需求,会发生什么?

Some example code:

一些示例代码:

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

And the resultant IL:

结果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 
    }

}

The key call is constrained !!T instead of box TestStruct, but the subsequent call is still callvirt in both cases.

关键调用受到限制!!T代替box TestStruct,但是在这两种情况下,后续调用仍然是callvirt。

So I don't know what it is with boxing that is required to make a virtual call, and I especially do not understand how using a generic constrained to a value type removes the need for the boxing operation.

因此,我不知道进行虚拟调用所需的装箱是什么,我尤其不理解如何使用一个受值类型约束的泛型来消除装箱操作的需要。

I thank everyone in advance...

我提前感谢大家……

5 个解决方案

#1


21  

My question, however, is how the compiler enables a generic constraint to eliminate the need for boxing a value type that explicitly implements an interface.

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

By "the compiler" it is not clear whether you mean the jitter or the C# compiler. The C# compiler does so by emitting the constrained prefix on the virtual call. See the documentation of the constrained prefix for details.

所谓的“编译器”,并不清楚你指的是抖动还是c#编译器。c#编译器通过在虚调用上发出受限的前缀来实现。有关详细信息,请参阅受约束前缀的文档。

What is going on with the behind-the-scenes CLR implementation that requires a value type to be boxed when accessing an explicitly implemented interface member

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

Whether the method being invoked is an explicitly implemented interface member or not is not particularly relevant. A more general question would be why does any virtual call require the value type to be boxed?

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

One traditionally thinks of a virtual call as being an indirect invocation of a method pointer in a virtual function table. That's not exactly how interface invocations work in the CLR, but it's a reasonable mental model for the purposes of this discussion.

传统上,虚拟调用被认为是对虚拟函数表中的方法指针的间接调用。这并不是CLR中接口调用的具体工作方式,但对于本文的目的来说,这是一个合理的心理模型。

If that's how a virtual method is going to be invoked then where does the vtable come from? The value type doesn't have a vtable in it. The value type just has its value in its storage. Boxing creates a reference to an object that has a vtable set up to point to all the value type's virtual methods. (Again, I caution you that this is not exactly how interface invocations work, but it is a good way to think about it.)

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

What happens with a generic constraint that removes this requirement?

如果通用约束删除了这个需求,会发生什么?

The jitter is going to be generating fresh code for each different value type argument construction of the generic method. If you're going to be generating fresh code for each different value type then you can tailor that code to that specific value type. Which means that you don't have to build a vtable and then look up what the contents of the vtable are! You know what the contents of the vtable are going to be, so just generate the code to invoke the method directly.

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

#2


6  

The ultimate goal is to get a pointer to the method table of the class so that the correct method can be called. That can't happen directly on a value type, it is just a blob of bytes. There are two ways to get there:

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

  • Opcodes.Box, implements the boxing conversion and turns the value type value into an object. The object has the method table pointer at offset 0.
  • 操作码。框,实现装箱转换并将值类型值转换为对象。对象的方法表指针位于偏移量0处。
  • Opcodes.Contrained, hands the jitter the method table pointer directly without the need for boxing. Enabled by the generic constraint.
  • 操作码。相反,不需要装箱,直接将方法表指针交给抖动器。由通用约束启用。

The latter is clearly more efficient.

后者显然更有效率。

#3


3  

Boxing is necessary when a value-type object is passed to a routine that expects to receive a class-type object. A method declaration like string ReadAndAdvanceEnumerator<T>(ref T thing) where T:IEnumerator<String> actually declares a whole family of functions, each of which expects a different type T. If T happens to be a value type (e.g. List<String>.Enumerator), the Just-In-Time compiler will actually generate machine code exclusively to perform ReadAndAdvanceEnumerator<List<String>.Enumerator>(). BTW, note the use of ref; if T were a class type (interface types used in any context other than constraints count as class types) the use of ref would be an unnecessary impediment to efficiency. If, however, there's a possibility that T might be a this-mutating struct (e.g. List<string>.Enumerator), the use of ref will be necessary to ensure that this mutations performed by the struct during the execution of ReadAndAdvanceEnumerator will be performed upon the caller's copy.

当值类型对象被传递给期望接收类类型对象的例程时,装箱是必要的。T:IEnumerator< string ReadAndAdvanceEnumerator (ref T)这样的方法声明,其中T:IEnumerator< string >实际上声明了一整套函数,每个函数都需要不同的类型T。顺便说一句,注意ref的使用;如果T是类类型(除了约束之外的任何上下文中使用的接口类型都算作类类型),那么使用ref将对效率造成不必要的障碍。但是,如果T可能是一个this- mustruct(例如List . enumerator),那么需要使用ref来确保在readandadvanceator执行过程中由struct执行的突变将在调用者的副本上执行。

#4


0  

I think you need to use

我认为你需要使用。

  • reflector
  • 反射器
  • ildasm / monodis
  • ildasm / monodis

to really get the answer you want

为了得到你想要的答案

You can of course look into the specs of the CLR (ECMA) and or the source of a C# compiler (mono)

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

#5


0  

The generic constraint provides only a compile time check that the correct type is being passed into the method. The end result is always that the compiler generates an appropriate method that accepts the runtime type:

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

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

In this sense, it bypasses the need for boxing of value types, because an explicit method instance is created that accepts that value type directly.

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

Whereas when a value type is cast to an implemented interface, the instance is a reference type, which is located on the heap. Because we don't take advantage of generics in this sense, we are forcing a cast to an interface (and subsequent boxing) if the runtime type is a value type.

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

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

Removal of the generic constraint only stops the compile time checking that your passing the correct type into the method.

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

#1


21  

My question, however, is how the compiler enables a generic constraint to eliminate the need for boxing a value type that explicitly implements an interface.

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

By "the compiler" it is not clear whether you mean the jitter or the C# compiler. The C# compiler does so by emitting the constrained prefix on the virtual call. See the documentation of the constrained prefix for details.

所谓的“编译器”,并不清楚你指的是抖动还是c#编译器。c#编译器通过在虚调用上发出受限的前缀来实现。有关详细信息,请参阅受约束前缀的文档。

What is going on with the behind-the-scenes CLR implementation that requires a value type to be boxed when accessing an explicitly implemented interface member

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

Whether the method being invoked is an explicitly implemented interface member or not is not particularly relevant. A more general question would be why does any virtual call require the value type to be boxed?

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

One traditionally thinks of a virtual call as being an indirect invocation of a method pointer in a virtual function table. That's not exactly how interface invocations work in the CLR, but it's a reasonable mental model for the purposes of this discussion.

传统上,虚拟调用被认为是对虚拟函数表中的方法指针的间接调用。这并不是CLR中接口调用的具体工作方式,但对于本文的目的来说,这是一个合理的心理模型。

If that's how a virtual method is going to be invoked then where does the vtable come from? The value type doesn't have a vtable in it. The value type just has its value in its storage. Boxing creates a reference to an object that has a vtable set up to point to all the value type's virtual methods. (Again, I caution you that this is not exactly how interface invocations work, but it is a good way to think about it.)

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

What happens with a generic constraint that removes this requirement?

如果通用约束删除了这个需求,会发生什么?

The jitter is going to be generating fresh code for each different value type argument construction of the generic method. If you're going to be generating fresh code for each different value type then you can tailor that code to that specific value type. Which means that you don't have to build a vtable and then look up what the contents of the vtable are! You know what the contents of the vtable are going to be, so just generate the code to invoke the method directly.

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

#2


6  

The ultimate goal is to get a pointer to the method table of the class so that the correct method can be called. That can't happen directly on a value type, it is just a blob of bytes. There are two ways to get there:

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

  • Opcodes.Box, implements the boxing conversion and turns the value type value into an object. The object has the method table pointer at offset 0.
  • 操作码。框,实现装箱转换并将值类型值转换为对象。对象的方法表指针位于偏移量0处。
  • Opcodes.Contrained, hands the jitter the method table pointer directly without the need for boxing. Enabled by the generic constraint.
  • 操作码。相反,不需要装箱,直接将方法表指针交给抖动器。由通用约束启用。

The latter is clearly more efficient.

后者显然更有效率。

#3


3  

Boxing is necessary when a value-type object is passed to a routine that expects to receive a class-type object. A method declaration like string ReadAndAdvanceEnumerator<T>(ref T thing) where T:IEnumerator<String> actually declares a whole family of functions, each of which expects a different type T. If T happens to be a value type (e.g. List<String>.Enumerator), the Just-In-Time compiler will actually generate machine code exclusively to perform ReadAndAdvanceEnumerator<List<String>.Enumerator>(). BTW, note the use of ref; if T were a class type (interface types used in any context other than constraints count as class types) the use of ref would be an unnecessary impediment to efficiency. If, however, there's a possibility that T might be a this-mutating struct (e.g. List<string>.Enumerator), the use of ref will be necessary to ensure that this mutations performed by the struct during the execution of ReadAndAdvanceEnumerator will be performed upon the caller's copy.

当值类型对象被传递给期望接收类类型对象的例程时,装箱是必要的。T:IEnumerator< string ReadAndAdvanceEnumerator (ref T)这样的方法声明,其中T:IEnumerator< string >实际上声明了一整套函数,每个函数都需要不同的类型T。顺便说一句,注意ref的使用;如果T是类类型(除了约束之外的任何上下文中使用的接口类型都算作类类型),那么使用ref将对效率造成不必要的障碍。但是,如果T可能是一个this- mustruct(例如List . enumerator),那么需要使用ref来确保在readandadvanceator执行过程中由struct执行的突变将在调用者的副本上执行。

#4


0  

I think you need to use

我认为你需要使用。

  • reflector
  • 反射器
  • ildasm / monodis
  • ildasm / monodis

to really get the answer you want

为了得到你想要的答案

You can of course look into the specs of the CLR (ECMA) and or the source of a C# compiler (mono)

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

#5


0  

The generic constraint provides only a compile time check that the correct type is being passed into the method. The end result is always that the compiler generates an appropriate method that accepts the runtime type:

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

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

In this sense, it bypasses the need for boxing of value types, because an explicit method instance is created that accepts that value type directly.

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

Whereas when a value type is cast to an implemented interface, the instance is a reference type, which is located on the heap. Because we don't take advantage of generics in this sense, we are forcing a cast to an interface (and subsequent boxing) if the runtime type is a value type.

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

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

Removal of the generic constraint only stops the compile time checking that your passing the correct type into the method.

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