我很想知道为什么会发生这种情况。 请阅读下面的代码示例以及每个部分下面的注释中发出的相应 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
这是否会造成类型安全问题?
请参阅 Eric Gunnerson 的 this 旧博客文章。
帖子正文如下:
为什么C#总是使用callvirt?
这个问题是在内部 C# 别名上提出的,我认为答案会引起普遍关注。假设答案是正确的 - 已经有一段时间了。
.NET IL 语言提供了 call 和 callvirt 指令,其中 callvirt 用于调用虚函数。但是,如果您查看 C# 生成的代码,您会发现即使在不涉及虚函数的情况下,它也会生成“callvirt”。为什么要这样做?
我回顾了我的语言设计笔记,它们非常清楚地表明我们决定在 1999 年 12 月 13 日使用 callvirt。不幸的是,他们没有理解我们这样做的理由,所以我将不得不从我的记忆中回忆起。
我们收到了某人(可能是使用 C# 的 .NET 小组之一(当时还没有命名为 C#))的报告,他编写了在空指针上调用方法的代码,但他们没有这样做得到一个异常,因为该方法没有访问任何字段(即“this”为空,但该方法中没有任何内容使用它)。然后该方法调用了另一个方法,该方法确实使用了 this 点并引发了异常,随后出现了一些令人头疼的情况。他们弄清楚后,给我们发了一张纸条。
我们认为能够在 null 实例上调用方法有点奇怪。 Peter Golde 做了一些测试,看看始终使用 callvirt 对性能的影响是什么,影响很小,因此我们决定进行更改。
谨慎行事。
从技术上讲,C# 编译器并不总是使用
callvirt
对于静态方法和值类型上定义的方法,它使用
call
。大多数是通过 callvirt
IL 指令提供的。
两者之间投票的区别在于,
call
假设“用于进行调用的对象”不为空。另一方面,callvirt
检查是否不为 null,并在需要时抛出 NullReferenceException。
call
用于它们 - 更好的性能。callvirt
,以便 JIT 编译器验证用于进行调用的对象不为空。即使对于非虚拟实例方法......他们也更看重安全性而不是性能。另请参阅:Jeff Richter 在这方面做得更好 - 在 CLR 中通过 C# 第二版的“设计类型”章节
编译器不知道第一个表达式中
o
的真实类型,但它确实知道第二个表达式中的真实类型。看起来它一次只看一个语句。
这很好,因为 C# 很大程度上依赖于 JIT 进行优化。在如此简单的情况下,两个调用很可能都会在运行时变成实例调用。
我不相信非虚拟方法会发出
callvirt
,但即使是这样,也没有问题,因为该方法永远不会被覆盖(出于明显的原因)。
我大胆猜测,这是因为第一个分配给一个变量,该变量可能包含另一种类型的向下转型实例,该实例可以覆盖
GetType
(尽管我们可以看到它没有);第二个永远只能是Object
。