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

Является ли Рихтер ошибочным при описании внутренних вызовов не виртуального метода?

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

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

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

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

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

И действительно, если вы ищете соответствующий 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();

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

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 во время выполнения.

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

4b9b3361

Ответ 1

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

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

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

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

Ответ 2

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

Метод VIRTUAL в базовом классе WILL имеет запись в таблице методов производного класса. Это означает, что все виртуальные методы типа "объект" доступны во всех таблицах методов их производных классов.

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

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

Метод ввода MethodDesc TableDe Имя JIT

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

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

50698360 503a6750 PreJIT System.Object.GetHashCode()

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

001b00c8 00143904 JIT Manager..ctor()

0014c065 001438f8 NONE Manager.GenProgressReport()

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

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

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

Ответ 3

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

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

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

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

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

Компилятор С# всегда вычисляет правильный метод для вызова и помещает эту информацию в токен метаданных. Поэтому 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(), приложение работает нормально.