2

可能重复:
为什么 C# 编译器会为 GetType() 方法调用发出 callvirt 指令?

我看到当我调用类的实例方法时,C# 编译器会发出 callvirt调用该方法的指令,为什么会这样?

这是否意味着所有实例方法都被编译器视为virtual methods,这是什么奥秘?

4

3 回答 3

14

它可以实现 C# 语言规范中的承诺。这说明通过空引用调用类的实例方法是不合法的。这听起来像是一个明显的特性,但实际上在 OOP 语言中并不常见。特别是 C++/CLI 编译器没有它。而 CLI 规范没有它。像 C++ 这样的非托管语言没有它。

有时,当实例方法不使用任何非静态类成员时,它甚至会达到一个好的结局。这种方法当然应该是静态的,但这不是必需的或强制的。

C# 要求非常好,它使NullReferenceException诊断更容易。由于它们是在调用站点而不是在实例方法内部生成的,因此它阐明了对象引用为空。找出方法中的this引用为 null 有点困难,尤其是因为您看不到它。由于地址实际上不是空的,因此更复杂的是,访问类的字段将生成一个从 0 偏移的地址。如果对象非常庞大,超过 64 KB,这反过来又是不安全的。访问如此大对象末尾的字段不一定会产生处理器异常,您只会读取随机垃圾。或者如果你写它会损坏内存。

因此,C# 团队寻找一种廉价的方法来实现空测试。并在callvirtIL 指令中找到了一个。与 不同的是call它确实在 CLI 规范中承诺了一个例外。一个非常便宜的测试,它只需要一条机器代码指令。并且不需要分支,如果处理器的分支预测逻辑猜错了,那将非常昂贵。

您现在也知道为什么 String.Equals() 包含这段神秘代码:

public override bool Equals(Object obj) {
    if (this == null)
        throw new NullReferenceException();
    // etc...
}
于 2013-01-11T18:35:14.953 回答
12

汉斯和迈克的答案是正确的。只是添加一点额外的信息:

  • callvirt指令在非虚拟方法上被明确记录为合法,以便您获得空检查行为。

  • 在 C# 编译器证明非虚拟调用不可能有空接收器的极少数情况下,它会回退到call并节省执行空检查所需的纳秒时间。例如,如果你有(new C()).InstanceMethod()then 应该生成为call,而不是callvirt因为编译器知道接收表达式永远不会为空。(如果分配失败,则抛出异常,因此永远不会执行调用。)

于 2013-01-11T19:47:11.937 回答
2

简短的回答:这样更安全。

callvirt将首先检查对象是否为空,如果是则抛出异常。

您会注意到调用静态方法仍将使用call,因为对象不能为空。

这里有一点历史。

于 2013-01-11T17:23:55.713 回答