我很想知道为什么会这样。请阅读下面的代码示例以及每个部分下面的注释中发出的相应IL:
using System;
class Program
{
static void Main()
{
Object o = new Object();
o.GetType();
// L_0001: newobj instance void [mscorlib]System.Object::.ctor()
// L_0006: stloc.0
// L_0007: ldloc.0
// L_0008: callvirt instance class [mscorlib]System.Type [mscorlib]System.Object::GetType()
new Object().GetType();
// L_000e: newobj instance void [mscorlib]System.Object::.ctor()
// L_0013: call instance class [mscorlib]System.Type [mscorlib]System.Object::GetType()
}
}
为什么编译器为第一部分发出callvirt
而第二部分发出call
?有没有理由编译器会为非虚方法发出callvirt
指令?如果在某些情况下编译器会为非虚拟方法发出callvirt
,这是否会造成类型安全问题?
答案 0 :(得分:28)
请参阅Eric Gunnerson撰写的this旧博文。
这是帖子的文字:
为什么C#总是使用callvirt?
这个问题出现在内部C#别名上,我认为答案是普遍感兴趣的。这是假设答案是正确的 - 已经有一段时间了。
.NET IL语言提供call和callvirt指令,callvirt用于调用虚函数。但是如果你仔细查看C#生成的代码,即使在没有涉及虚函数的情况下,你也会看到它生成一个“callvirt”。为什么这样做?
我回过头来看过我的语言设计笔记,他们非常清楚地说明我们决定在12/13/1999使用callvirt。不幸的是,他们没有抓住我们这样做的理由,所以我将不得不离开我的记忆。
我们收到了某人的报告(可能是一个使用C#的.NET组(当时认为它还没有被命名为C#))谁编写了一个在空指针上调用方法的代码,但他们没有得到一个异常,因为该方法没有访问任何字段(即“this”为null,但方法中没有使用它)。那个方法然后调用另一个方法,它确实使用了这一点并引发了一个异常,随后出现了一些令人头疼的问题。在他们弄明白之后,他们给我们发了一张关于它的说明。
我们认为能够在null实例上调用方法有点奇怪。 Peter Golde做了一些测试,看看使用callvirt的性能影响是什么,而且它足够小,我们决定做出改变。
答案 1 :(得分:22)
只是玩得安全。
从技术上讲,C#编译器不总是使用callvirt
静态方法&在值类型上定义的方法,它使用call
。大多数是通过callvirt
IL指令提供的。
在两者之间进行投票的区别在于call
假设“用于进行调用的对象”不为空。另一方面,callvirt
检查not null并在必要时抛出NullReferenceException。
call
用于他们 - 更好的表现。callvirt
,因此JIT编译器会验证用于进行调用的对象是否为空。即使对于非虚拟实例方法,他们也重视安全性而非性能。参见:Jeff Richter在这方面做得更好 - 在CLR中的“设计类型”一章中通过C#2nd Ed
答案 2 :(得分:3)
作为一个(或许)有趣的... ... GetType()
是不寻常的,因为不是 virtual
- 这会导致一些very, very odd things。
(标记为维基,因为它与实际问题有些偏离主题)
答案 3 :(得分:1)
编译器不知道第一个表达式中o
的实际类型,但它知道第二个表达式中的实际类型。看起来它一次只看一个陈述。
这很好,因为C#在很大程度上依赖于JIT进行优化。在这么简单的情况下,两个调用都很可能在运行时成为实例调用。
我不相信非虚拟方法会发出callvirt
,但即使是这样,也没有问题,因为该方法永远不会被覆盖(原因很明显)。
答案 4 :(得分:0)
我会冒这个猜测,因为第一个分配给一个变量,它可能包含一个可以覆盖GetType
的另一个类型的下载实例(即使我们看不到它);第二种情况永远不会是Object
。