通用约束如何防止使用隐式实现的接口对值类型进行装箱?
我的问题与此有点相关:显式实现的接口和通用约束。
然而,我的问题是编译器如何启用通用约束来消除对显式实现接口的值类型进行装箱的需要。
我想我的问题可以归结为两部分:
-
在访问显式实现的接口成员时需要对值类型进行装箱的幕后 CLR 实现发生了什么,以及
消除此要求的泛型约束会发生什么?
一些示例代码:
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
在这两种情况下。
因此,我不知道进行虚拟调用所需的装箱是什么,而且我特别不明白如何使用受限于值类型的泛型来消除装箱操作的需要。
我提前感谢大家...
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(5)
不清楚“编译器”是指抖动还是 C# 编译器。 C# 编译器通过在虚拟调用上发出约束前缀来实现此目的。请参阅约束前缀的文档了解详情。
调用的方法是否是显式实现的接口成员并不特别相关。一个更普遍的问题是为什么任何虚拟调用都需要对值类型进行装箱?
传统上,人们认为虚拟调用是对虚拟函数表中方法指针的间接调用。这并不完全是接口调用在 CLR 中的工作方式,但对于本次讨论来说,这是一个合理的思维模型。
如果这就是调用虚拟方法的方式,那么 vtable 从何而来?值类型中没有 vtable。值类型仅在其存储中具有其值。装箱创建对对象的引用,该对象具有设置为指向所有值类型的虚拟方法的 vtable。 (我再次提醒您,这并不完全是接口调用的工作方式,但这是一个很好的思考方式。)
抖动将为通用方法的每个不同值类型参数构造生成新鲜代码。如果您要为每种不同的值类型生成新的代码,那么您可以根据该特定的值类型定制该代码。这意味着您不必构建 vtable,然后查找 vtable 的内容是什么!您知道 vtable 的内容是什么,因此只需生成代码来直接调用该方法即可。
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.
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.)
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.
最终的目标是获得类的方法表的指针,以便调用正确的方法。这不能直接发生在值类型上,它只是一个字节块。有两种方法可以实现:
后者显然效率更高。
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:
The latter is clearly more efficient.
当将值类型对象传递给期望接收类类型对象的例程时,装箱是必要的。像
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 typeT
. IfT
happens to be a value type (e.g.List<String>.Enumerator
), the Just-In-Time compiler will actually generate machine code exclusively to performReadAndAdvanceEnumerator<List<String>.Enumerator>()
. BTW, note the use ofref
; ifT
were a class type (interface types used in any context other than constraints count as class types) the use ofref
would be an unnecessary impediment to efficiency. If, however, there's a possibility thatT
might be athis
-mutating struct (e.g.List<string>.Enumerator
), the use ofref
will be necessary to ensure thatthis
mutations performed by the struct during the execution ofReadAndAdvanceEnumerator
will be performed upon the caller's copy.泛型约束仅提供编译时检查,以确保正确的类型被传递到方法中。最终结果始终是编译器生成接受运行时类型的适当方法:
从这个意义上说,它绕过了对值类型装箱的需要,因为创建了直接接受该值类型的显式方法实例。
而当值类型转换为实现的接口时,实例是引用类型,位于堆上。因为我们在这个意义上没有利用泛型,所以如果运行时类型是值类型,我们将强制转换为接口(以及随后的装箱)。
删除泛型约束只会停止编译时检查您是否将正确的类型传递到方法中。
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:
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.
Removal of the generic constraint only stops the compile time checking that your passing the correct type into the method.
我认为你需要使用
才能真正得到你想要的答案
你当然可以查看 CLR (ECMA) 的规范和/或 C# 编译器的源代码 (单声道)
I think you need to use
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)