28

我很想知道为什么会这样。请阅读下面的代码示例以及在每个部分下方的注释中发出的相应 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为第一部分发出 a 而call为第二部分发出 a ?编译器是否有任何理由会callvirt为非虚拟方法发出指令?如果在某些情况下编译器会callvirt为非虚拟方法发出 a ,这是否会给类型安全带来问题?

4

5 回答 5

30

请参阅Eric Gunnerson 的这篇旧博客文章。

这是帖子的正文:

为什么 C# 总是使用 callvirt?

这个问题出现在一个内部 C# 别名上,我认为答案会引起普遍关注。这是假设答案是正确的——已经有一段时间了。

.NET IL 语言同时提供 call 和 callvirt 指令,其中 callvirt 用于调用虚函数。但是,如果您查看 C# 生成的代码,您会发现即使在不涉及虚函数的情况下,它也会生成“callvirt”。为什么这样做?

我回顾了我的语言设计说明,它们非常清楚地表明我们决定在 1999 年 12 月 13 日使用 callvirt。不幸的是,他们没有抓住我们这样做的理由,所以我将不得不从我的记忆中消失。

我们从某个人(可能是使用 C# 的 .NET 小组之一(当时还没有命名为 C#))那里得到了一份报告,他编写了在空指针上调用方法的代码,但他们没有得到一个异常,因为该方法没有访问任何字段(即“this”为空,但方法中没有使用它)。然后那个方法调用了另一个方法,它确实使用了这个点并抛出了一个异常,随之而来的是一些令人头疼的问题。在他们弄清楚之后,他们给我们发了一张关于它的便条。

我们认为能够在空实例上调用方法有点奇怪。Peter Golde 做了一些测试,看看总是使用 callvirt 对性能的影响是什么,它足够小,我们决定做出改变。

于 2009-05-10T17:11:27.553 回答
23

只是玩安全。

从技术上讲,C# 编译器并不总是使用callvirt

对于在值类型上定义的静态方法和方法,它使用call. 大多数是通过callvirtIL 指令提供的。

两者之间的不同之处在于call假设“用于进行调用的对象”不为空。callvirt另一方面,检查 not null 并在需要时抛出 NullReferenceException。

  • 对于静态方法,对象是类型对象,不能为空。值类型同上。因此call用于他们 - 更好的性能。
  • 对于其他人,语言设计者决定采用,callvirt因此 JIT 编译器验证用于进行调用的对象不为空。即使对于非虚拟实例方法.. 他们重视安全而不是性能。

另请参阅:Jeff Richter 在这方面做得更好 - 在他通过 C# 2nd Ed 在 CLR 中的“设计类型”一章中

于 2009-05-10T17:32:20.347 回答
3

作为一个(也许-)有趣的旁白......GetType()是不寻常的,因为它不是 virtual- 这会导致一些非常非常奇怪的事情

(标记为 wiki,因为它与实际问题有些偏离主题)

于 2009-05-10T20:27:11.623 回答
2

编译器不知道o第一个表达式中的真实类型,但它知道第二个表达式中的真实类型。看起来它一次只查看一个语句。

这很好,因为 C# 在很大程度上依赖于 JIT 进行优化。在这种简单的情况下,两个调用很可能在运行时都成为实例调用。

我不相信callvirt曾经为非虚拟方法发出过,但即使是这样,也没有问题,因为该方法永远不会被覆盖(出于显而易见的原因)。

于 2009-05-10T17:08:42.580 回答
0

我会冒险猜测这是因为第一个分配给一个变量,该变量可能包含另一个类型的向下转换实例,该实例可能已经被覆盖GetType(即使我们可以看到它没有);第二个永远不会是Object.

于 2009-05-10T17:10:07.510 回答