为什么 C# 编译器为 GetType() 方法调用发出 callvirt 指令?

问题描述 投票:0回答:5

我很想知道为什么会发生这种情况。 请阅读下面的代码示例以及每个部分下面的注释中发出的相应 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
这是否会造成类型安全问题?

c# cil type-safety
5个回答
30
投票

请参阅 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 对性能的影响是什么,影响很小,因此我们决定进行更改。


23
投票

谨慎行事。

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

callvirt

对于静态方法和值类型上定义的方法,它使用

call
。大多数是通过
callvirt
IL 指令提供的。

两者之间投票的区别在于,

call
假设“用于进行调用的对象”不为空。另一方面,
callvirt
检查是否不为 null,并在需要时抛出 NullReferenceException。

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

另请参阅:Jeff Richter 在这方面做得更好 - 在 CLR 中通过 C# 第二版的“设计类型”章节


3
投票

作为一个(也许-)有趣的旁白......

GetType()
的不寻常之处在于它不是
virtual
- 这会导致一些非常非常奇怪的事情

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


2
投票

编译器不知道第一个表达式中

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

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

我不相信非虚拟方法会发出

callvirt
,但即使是这样,也没有问题,因为该方法永远不会被覆盖(出于明显的原因)。


0
投票

我大胆猜测,这是因为第一个分配给一个变量,该变量可能包含另一种类型的向下转型实例,该实例可以覆盖

GetType
(尽管我们可以看到它没有);第二个永远只能是
Object

© www.soinside.com 2019 - 2024. All rights reserved.