为什么callvirt IL指令会导致虚拟方法中的递归调用?

时间:2012-04-18 16:06:50

标签: c# .net cil

在这种情况下,IL并不总是对callvirt方法使用virtual指令:

class MakeMeASandwich{
  public override string ToString(){
    return base.ToString();
  }
}

在这种情况下,据说IL将生成call而非callvirt,其中生成callvirt以检查variable是否为null否则会抛出NullReferenceException

  1. 如果使用callvirt代替call,为什么会在堆栈溢出之前发生递归调用?
  2. 如果使用call,那么它何时检查它用于调用方法的实例变量是否为空?

5 个答案:

答案 0 :(得分:10)

  

如果使用callvirt而不是call,如果堆栈溢出,为什么会发生递归调用?

因为那时你的代码完全相同:

override string ToString()
{
    return this.ToString();
}

这显然是无限递归,只要给出的方法是ToString最重要的版本。

  

如果使用了call,那怎么检查它用来调用方法的实例变量是否为null?

这个问题无法回答,因为这个问题假设是错误的。调用指令检查以查看对接收器的引用是否为空,因此询问调用指令检查null 的原因没有任何意义。< / p>

让我重新解释一下这些更好的问题:

  

在什么情况下C#编译器会生成一个呼叫而不是一个callvirt?

如果C#代码在虚拟方法上进行非虚拟呼叫,则编译器必须生成呼叫,而不是callvirt。这种情况发生的唯一时间是使用base来调用虚方法。

如果C#代码正在进行虚拟调用,那么编译器必须生成一个callvirt。

如果C#代码在非虚拟方法上进行非虚拟调用,则编译器可以选择生成call或callvirt。要么工作。 C#编译器通常选择生成一个callvirt。

  

调用指令不会自动执行空检查,但是callvirt会执行。如果C#编译器选择生成调用而不是callvirt,它是否也有义务生成空检查?

没有。如果已知接收器不为空,则C#编译器可以跳过空检查。例如,如果您对非虚方法M说(new C()).M()那么编译器生成call指令而不进行空检查是合法的。我们知道(1)该方法不是虚拟的,因此它不必是callvirt;我们可以选择是否使用callvirt。我们知道(2)new C()永远不会为空,所以我们不必生成空检查。

如果C#编译器知道接收者不是null,那么它将生成一个callvirt,或者它将生成一个空检查,然后调用。

答案 1 :(得分:3)

  1. callvirt将调用MakeMeASandwich实现,而不是Object实现。这就是你的堆栈溢出的方法。

  2. 最初的调用是使用callvirt,它确定引用不为null。如果控件在此ToString实现中,则您已经知道有一个对象。

答案 2 :(得分:3)

    使用了
  1. call,因为它知道调用哪个方法,并且不应该在运行时查找它(callvirt会导致代码调用在最具体的类,然后导致堆栈溢出。

  2. callvirt表示空检查,而call则不表示。

答案 3 :(得分:2)

Callvirt调用可用的派生方法。在这种情况下,这是MakeMeASandwich.ToString()。

callvirt的目的不仅是检查null,还要执行虚方法调用。

答案 4 :(得分:2)

  

如果使用callvirt而不是call?

,为什么在堆栈溢出之前会发生递归调用

正如其他人所回答的那样,callvirt虚拟地调用了该方法。这就像你写的那样

public override string ToString() {
    return ToString();
}
  

如果使用了call,那怎么检查它用来调用方法的实例变量是否为null?

其他人提到你已经知道this不是空的。那是不对的。 如果 callvirt用于调用MakeMeASandwich.ToString,则是,this不能为空。但是,不要求callvirt用于调用您的函数。其他语言允许您以生成call操作码的方式编写调用,在这种情况下,不执行空检查。我认为C ++ / CLI允许它makeMeASandwich->MakeMeASandwich::ToString(),但我不完全确定。除非您检查,否则您无法确定this不为空。