Ошибается ли Рихтер при описании внутренних элементов вызова не виртуального метода? - PullRequest
12 голосов
/ 05 февраля 2011

Я бы написал этот вопрос напрямую Джеффри Рихтеру, но в прошлый раз он не ответил мне :), поэтому я постараюсь получить ответ с вашей помощью, ребята:)

В книге "CLR via C #", 3-е издание, на стр.108 Джеффри пишет:

void M3() {
  Employee e;
  e = new Manager();
  year = e.GetYearsEmployed();
  ...
}

Следующая строка кода в вызовах M3 Не виртуальный экземпляр сотрудника GetYearsEmployed метод. При звонке не виртуальный метод экземпляра, JIT Компилятор находит объект типа, который соответствует типу переменная, используемая для вызова. В этом случае переменная е определяется как сотрудник. ( Если Тип сотрудника не определил метод вызываемый компилятор JIT вниз по иерархии классов в сторону объекта ищу этот метод. это можно сделать это потому, что каждый тип объекта имеет поле в нем, что относится к его базе тип; эта информация не отображается в цифры.) Затем JIT-компилятор находит запись в объекте типа таблица методов, которая относится к методу вызывается, JITs метод (если необходимо), а затем вызывает JITted Код.

Когда я прочитал это впервые, я подумал, что было бы неэффективно идти по иерархии классов в поисках метода во время JIT-тинга. Легко найти метод уже на этапе компиляции. Но я поверил Джеффри. Я разместил эту информацию на другом форуме, и другой парень подтвердил мои сомнения, что это странно и будет неэффективно, и, похоже, это неверная информация.

И действительно, если вы посмотрите соответствующий код IL в декомпиляторе, таком как ILDasm или Reflector (я проверял оба), вы увидите, что IL имеет инструкцию callvirt, вызывающую метод из базового класса, поэтому JIT не нужно искать, в каком классе находится метод во время выполнения:

public class EmployeeBase
{
    public int GetYearsEmployed() { return 1; }
}

public class Employee : EmployeeBase
{
    public void SomeOtherMethod() { }
}

public class Manager : Employee
{
    public void GenProgressReport() { }
}

...

Employee e;
e = new Manager();
int years = e.GetYearsEmployed();

Результирующий ИЛ:

L_0000: nop 
L_0001: newobj instance void TestProj.Form1/Manager::.ctor()
L_0006: stloc.0 
L_0007: ldloc.0 
L_0008: callvirt instance int32 TestProj.Form1/EmployeeBase::GetYearsEmployed()

Понимаете? Компилятор уже обнаружил, что метод находится не в классе Employee, а в классе EmployeeBase, и испустил правильный вызов. Но, по словам Рихтера, JIT должен был бы выяснить, что метод на самом деле находится в классе EmployeeBase во время выполнения.

Джеффри Рихтер ошибся? Или я чего-то не понимаю?

Ответы [ 3 ]

2 голосов
/ 24 июля 2013

Компилятор C # разрешает не виртуальные методы точно без места для маневра. Если производный не виртуальный метод с той же сигнатурой возникнет после компиляции вызывающей стороны, CLR все равно вызовет «фиксированный» метод, выбранный компилятором C #. Это сделано для того, чтобы избежать хрупкой проблемы базового класса.

Если вы хотите динамическое разрешение метода , используйте virtual. Если вы не используете virtual, вы получите полностью статическое разрешение . Твой выбор. Тип времени выполнения ссылки на объект, который становится указателем this, вообще не имеет значения при разрешении не виртуальных методов (ни для csc.exe, ни для CLR JIT).

JIT всегда будет вызывать точно выбранный метод. выдаст исключение, если метод не существует (возможно, из-за того, что DLL вызываемого пользователя была изменена). Он не будет вызывать другой метод.

callvirt также может вызывать не виртуальные методы. Используется для проверки null . Это определяется таким образом, а C # определяется для выполнения нулевой проверки при каждом вызове.

0 голосов
/ 24 июля 2013

Как ответил @usr в похожем вопросе, который я написал Как разрешается наследование метода не виртуального экземпляра? :

Время выполнения обычно означает «когда / каждый раз выполняется код»».Разрешение JIT здесь задействовано только один раз до запуска кода.То, что делает JIT, не упоминается словами «во время выполнения».

Также в словах Джеффри

JIT-компилятор находит объект типа, соответствующий типупеременная, используемая для выполнения вызова.

Тип переменной здесь, на мой взгляд, означает «класс, определенный токеном метаданных» (вызов ECMA 335 III.3.19), на основе которого JIT разрешает назначение метода.

Компилятор C # всегда определяет правильный метод для вызова и помещает эту информацию в токен метаданных.Так что JIT никогда не должен «идти по иерархии классов».(Но это возможно, если вы вручную измените токен метаданных на унаследованный метод)

    class A
    {
        public static void Foo() {Console.WriteLine(1); }
        public void Bar() { Console.WriteLine(2); }
    }
    class B : A {}
    class C : B {}

    static void Main()
    {
        C.Foo();
        new C().Bar(); 
        C x = new C();
        x.Bar();
        Console.ReadKey();
    }

IL_0000:  call       void ConsoleApplication5.Program/A::Foo() // change to B::Foo()
IL_0005:  newobj     instance void ConsoleApplication5.Program/C::.ctor()
IL_000a:  call       instance void ConsoleApplication5.Program/A::Bar() // change to B::Bar()
IL_000f:  newobj     instance void ConsoleApplication5.Program/C::.ctor()
IL_0014:  stloc.0
IL_0015:  ldloc.0
IL_0016:  callvirt   instance void ConsoleApplication5.Program/A::Bar() // change to B::Bar()
IL_001b:  call       valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey()
IL_0020:  pop
IL_0021:  ret

Если мы используем Ildasm + Ilasm, чтобы изменить A::Foo() на B::Foo() и изменить A::Bar() на B.Bar() приложение работает нормально.

0 голосов
/ 02 января 2013

Из моего понимания и использования вашего примера: Под капотом:

Виртуальный метод в базовом классе БУДЕТ иметь запись в таблице методов производного класса.Это означает, что все виртуальные методы типа «объект» доступны во всех таблицах методов их производных классов.

НЕ Виртуальный метод (как в примере кода), без предоставленных функций впроизводные классы фактически НЕ будут иметь записи в таблицах методов производных классов!

Чтобы проверить это, я запустил код в WinDbg, чтобы проверить таблицу методов для класса Manager .

Запись в таблице MethodDesc Имя метода JD MethodDe

506a4960 503a6728 PreJIT System.Object.ToString ()

50698790 503a6730 PreJIT System.Object.Equals (System.Object)

50698360 503a6750 PreJIT System.Object.)

506916f0 503a6764 PreJIT System.Object.Finalize ()

001b00c8 00143904 JIT Manager..ctor ()

0014c065 001438f8 НЕТ Manager.GenProgressReport ()

Итак, я могу видеть методы объекта для виртуального объекта, но не вижу фактического метода GetYearsEmployed, поскольку он не является виртуальным и не имеет производной реализации.Кстати, по той же концепции вы не можете увидеть функцию SomeOtherMethod в производном классе.

Однако вы можете вызывать эти функции, просто их нет в таблице методов.Я могу ошибаться, но я верю, что стек вызовов обходит их.Может быть, это то, что мистер Рихтер имеет в виду в своей книге.Мне трудно читать его книгу, но это потому, что понятия сложны, а он умнее меня :)

Я не уверен, что IL отражает проблему.Я полагаю, что это возможно слой ниже IL, поэтому я использовал Windbg, чтобы посмотреть.Я полагаю, вы могли бы использовать windbg, чтобы увидеть, как он обходит стек ....

...