Подтвердить что ты не робот

Почему компилятор С# производит вызов метода для вызова метода BaseClass в IL

Предположим, что у нас есть следующий пример кода в С#:

class BaseClass
  {
    public virtual void HelloWorld()
    {
      Console.WriteLine("Hello Tarik");
    }
  }

  class DerivedClass : BaseClass
  {
    public override void HelloWorld()
    {
      base.HelloWorld();
    }
  }

  class Program
  {
    static void Main(string[] args)
    {
      DerivedClass derived = new DerivedClass();
      derived.HelloWorld();
    }
  }

Когда я прочитал следующий код:

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       15 (0xf)
  .maxstack  1
  .locals init ([0] class EnumReflection.DerivedClass derived)
  IL_0000:  nop
  IL_0001:  newobj     instance void EnumReflection.DerivedClass::.ctor()
  IL_0006:  stloc.0
  IL_0007:  ldloc.0
  IL_0008:  callvirt   instance void EnumReflection.BaseClass::HelloWorld()
  IL_000d:  nop
  IL_000e:  ret
} // end of method Program::Main

Однако csc.exe преобразован derived.HelloWorld();callvirt instance void EnumReflection.BaseClass::HelloWorld(). Почему это? Я не упоминал BaseClass нигде в методе Main.

А также, если он вызывает BaseClass::HelloWorld(), тогда я ожидал бы call вместо callvirt, поскольку он смотрит прямо на метод BaseClass::HelloWorld().

4b9b3361

Ответ 1

Вызов идет в BaseClass:: HelloWorld, потому что BaseClass - это класс, который определяет метод. Способ виртуальной диспетчеризации в С# заключается в том, что метод вызывается в базовом классе, а виртуальная диспетчерская система отвечает за то, что вызывается вызываемый метод с наивысшим значением.

Этот ответ Эрика Липперта очень информативен: fooobar.com/questions/437593/...

Как и его серия в блоге по теме: http://blogs.msdn.com/b/ericlippert/archive/tags/virtual+dispatch/

У вас есть идея, почему это реализовано таким образом? Что произойдет, если бы он вызывал метод производного класса ToString напрямую? Этот способ не очень меня это понял на первый взгляд...

Он реализован таким образом, потому что компилятор не отслеживает тип среды выполнения, а только тип времени их компиляции. С кодом, который вы опубликовали, легко видеть, что вызов будет передан в реализацию метода DerivedClass. Но предположим, что переменная derived была инициализирована следующим образом:

Derived derived = GetDerived();

Возможно, что GetDerived() возвращает экземпляр StillMoreDerived. Если StillMoreDerived (или любой класс между derived и StillMoreDerived в цепочке наследования) переопределяет этот метод, было бы неверно вызывать реализацию метода derived.

Чтобы найти все возможные значения, которые может иметь переменная через статический анализ, необходимо решить проблему остановки. С сборкой .NET проблема еще хуже, потому что сборка может быть не полной программой. Таким образом, количество случаев, когда компилятор мог обоснованно доказать, что derived не содержит ссылки на более производный объект (или нулевую ссылку), будет небольшим.

Сколько стоило бы добавить эту логику, чтобы она могла выдавать инструкцию call, а не callvirt? Несомненно, стоимость будет намного выше, чем полученная небольшая выгода.

Ответ 2

Способ думать об этом заключается в том, что виртуальные методы определяют "слот", который вы можете поместить во время выполнения. Когда мы издаем инструкцию callvirt, мы говорим "во время выполнения, посмотрите, что находится в этом слоте и вызывают его".

Слот идентифицируется с помощью информации о методе типа, объявляющего виртуальный метод, а не типа, который его переопределяет.

Было бы совершенно законно испускать callvirt для производного метода; во время выполнения будет понятно, что производный метод является тем же слотом, что и базовый метод, и результат будет точно таким же. Но для этого никогда не было никаких оснований. Более понятно, если мы идентифицируем слот, идентифицируя тип, объявляющий этот слот.

Ответ 3

Обратите внимание, что это происходит, даже если вы объявляете DerivedClass как sealed.

С# использует оператор callvirt для вызова любого метода экземпляра (virtual или нет), чтобы автоматически получить нулевую проверку ссылки на объект - поднять NullReferenceException в точке вызова метода. В противном случае NullReferenceException будет поднят только при первом действительном использовании любого члена экземпляра класса внутри метода, что может быть неожиданным. Если ни один экземпляр не используется, метод действительно может завершиться успешно, даже не создавая исключения.

Вы также должны помнить, что IL не выполняется напрямую. Он сначала компилируется с помощью собственных команд компилятором JIT и выполняет ряд оптимизаций в зависимости от того, отлаживаете ли вы процесс. Я обнаружил, что x86 JIT для CLR 2.0 ввел не виртуальный метод, но назвал виртуальный метод - он также был встроен Console.WriteLine!