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

发布于 2024-10-29 23:51:07 字数 3703 浏览 4 评论 0 原文

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

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

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

  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在这两种情况下。

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

我提前感谢大家...

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

  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:

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

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

太阳公公是暖光 2024-11-05 23:51:07

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

不清楚“编译器”是指抖动还是 C# 编译器。 C# 编译器通过在虚拟调用上发出约束前缀来实现此目的。请参阅约束前缀的文档了解详情。

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

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

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

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

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

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

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.

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

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.

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

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.

瑶笙 2024-11-05 23:51:07

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

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

后者显然效率更高。

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

游魂 2024-11-05 23:51:07

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

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.

月野兔 2024-11-05 23:51:07

泛型约束仅提供编译时检查,以确保正确的类型被传递到方法中。最终结果始终是编译器生成接受运行时类型的适当方法:

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

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

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.

单挑你×的.吻 2024-11-05 23:51:07

我认为你需要使用

  • 反射器
  • ildasm / monodis

才能真正得到你想要的答案

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

I think you need to use

  • reflector
  • 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)

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文