对于以下代码段:
struct Test
{
public override string ToString()
{
return "";
}
}
public class Program
{
public static void Main()
{
Test a = new Test();
a.ToString();
Int32 b = 5;
b.ToString();
}
}
编译器发出以下IL:
.locals init ([0] valuetype ConsoleApplication2.Test a,
[1] int32 b)
IL_0000: nop
IL_0001: ldloca.s a
IL_0003: initobj ConsoleApplication2.Test
IL_0009: ldloca.s a
IL_000b: constrained. ConsoleApplication2.Test
IL_0011: callvirt instance string [mscorlib]System.Object::ToString()
IL_0016: pop
IL_0017: ldc.i4.5
IL_0018: stloc.1
IL_0019: ldloca.s b
IL_001b: call instance string [mscorlib]System.Int32::ToString()
IL_0020: pop
IL_0021: ret
由于值类型Test
和Int32
都会覆盖ToString()
方法,因此我认为a.ToString()
和b.ToString()
都不会发生装箱。因此,我想知道为什么编译器为constraned
发出callvirt
+ Test
,为call
发出Int32
?
答案 0 :(得分:6)
这是编译器对基本类型进行的优化。
但即使对于自定义结构,callvirt
实际上也会在运行时由call
操作码执行constrained.
- 在方法被覆盖的情况下。它允许编译器在任何一种情况下发出相同的指令,并让运行时处理它。
来自MSDN:
如果
thisType
是值类型且thisType
实现method
,那么ptr
将作为this
指针未经修改地传递给 {{1}方法指令,用于call
的方法实现。
和
thisType
操作码允许IL编译器以统一的方式调用虚函数,与constrained
是值类型还是引用类型无关。虽然它适用于ptr
是泛型类型变量的情况,但约束前缀也适用于非泛型类型,并且可以降低在隐藏值类型和引用类型之间区别的语言中生成虚拟调用的复杂性。 / p>
我不知道有关优化的任何官方文档,但您可以在Roslyn仓库中看到MayUseCallForStructMethod
method的评论。
至于为什么这种优化被推迟到非原始类型的运行时,我相信这是因为实现可以改变。想象一下,引用一个最初覆盖thisType
的库,然后将DLL(不重新编译!)更改为删除覆盖的库。这会导致运行时异常。对于原语,他们可以肯定它不会发生。
答案 1 :(得分:0)
这是因为serialize
是一个提供密封类型的框架,并且永远不会发生某些其他类型覆盖int serialize
方法,因此编译器知道它总是需要调用Int
ToString
类型提供的方法实现,因此不需要使用ToString()
来确定要调用的实现。
对于primitve类型编译器知道要调用int
的哪个实现,但是当我们创建自定义值类型时,它是一个以前从未存在的新类型,因此编译器不知道它和它需要弄清楚要调用哪个实现以及它所在的位置,因为它默认从callvirt
继承,因此编译器必须ToString
找到为自定义提供的Object
实现如果不重写,它将调用显式的Object类型。
以下现有的SO帖子可以帮助您理解这一点: