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

Почему компилятор С# испускает инструкцию callvirt для вызова метода GetType()?

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

using System;

class Program
{
    static void Main()
    {
        Object o = new Object();
        o.GetType();

        // L_0001: newobj instance void [mscorlib]System.Object::.ctor()
        // L_0006: stloc.0 
        // L_0007: ldloc.0 
        // L_0008: callvirt instance class [mscorlib]System.Type [mscorlib]System.Object::GetType()

        new Object().GetType();

        // L_000e: newobj instance void [mscorlib]System.Object::.ctor()
        // L_0013: call instance class [mscorlib]System.Type [mscorlib]System.Object::GetType()
    }
}

Почему компилятор выбрал callvirt для первого раздела, но call для второго раздела? Есть ли причина, по которой компилятор когда-либо выдавал инструкцию callvirt для не виртуального метода? И если есть случаи, когда компилятор будет генерировать callvirt для не виртуального метода, это создает проблемы безопасности типов?

4b9b3361

Ответ 1

Просто безопасно играть.

Технически компилятор С# не всегда использует callvirt

Для статических методов и методов, определенных для типов значений, он использует call. Большинство предоставляется через инструкцию callvirt IL.

Разница, вызвавшая голосование между двумя, заключается в том, что call предполагает, что "объект, используемый для совершения вызова", не равен нулю. callvirt, с другой стороны, проверяет, не имеет значения null и, если требуется, выбрасывает исключение NullReferenceException.

  • Для статических методов объект является объектом типа и не может быть нулевым. Тоже для типов значений. Следовательно, для них используется call - лучшая производительность.
  • Для остальных разработчики языка решили пойти с callvirt, поэтому компилятор JIT проверяет, что объект, используемый для совершения вызова, не является нулевым. Даже для не виртуальных методов экземпляров.. они ценят безопасность по производительности.

См. также: Джефф Рихтер лучше справляется с этим - в главе "Типы дизайна" в CLR через С# 2nd Ed

Ответ 2

Смотрите этот старый пост в блоге Эрика Гуннерсона.

Вот текст сообщения:

Почему С# всегда использует callvirt?

Этот вопрос возник на внутреннем псевдониме С#, и я подумал, что ответ будет представлять общий интерес. Это предполагает, что ответ правильный - это было довольно долгое время.

Язык .NET IL предоставляет команду вызова и callvirt, при этом callvirt используется для вызова виртуальных функций. Но если вы просмотрите код, который генерирует С#, вы увидите, что он генерирует "callvirt" даже в тех случаях, когда виртуальная функция не задействована. Почему он это делает?

Я вернулся с помощью замечаний по дизайну языка, которые у меня есть, и они совершенно ясно заявили, что решили использовать callvirt 12/13/1999. К сожалению, они не фиксируют наше обоснование для этого, поэтому мне придется уйти из моей памяти.

Мы получили отчет от кого-то (вероятно, одна из групп .NET, использующая С# (думала, что еще не названа С# в то время)), который написал код, который вызвал метод на нулевом указателе, но они didnt получить исключение, потому что метод не имел доступа к каким-либо полям (т.е. "this" был null, но ничто в используемом им методе). Этот метод затем вызвал другой метод, который использовал эту точку, и бросил исключение, и последовало немного царапин на голове. После того, как они поняли это, они отправили нам записку об этом.

Мы думали, что возможность вызова метода в экземпляре null была немного странной. Питер Голд сделал несколько тестов, чтобы понять, какое влияние на первичность было всегда на использование callvirt, и он был достаточно мал, чтобы мы решили внести изменения.

Ответ 3

Как (возможно,) интересный в стороне... GetType() необычен тем, что он не virtual - это приводит к некоторым очень, очень странным вещам.

(помечено как wiki, поскольку оно немного не соответствует теме)

Ответ 4

Компилятор не знает реальный тип o в первом выражении, но он знает истинный тип во втором выражении. Похоже, он смотрит только на одно утверждение за раз.

Это прекрасно, потому что С# сильно зависит от JIT для оптимизации. Скорее всего, в таком простом случае, что оба вызова станут вызовами экземпляра во время выполнения.

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

Ответ 5

Я бы поставил под угрозу, что это связано с тем, что первый присваивает переменной, которая потенциально может содержать свернутый экземпляр другого типа, который мог бы переопределить GetType (хотя мы видим, что это не так); второй никогда не может быть чем-то другим, кроме Object.