Почему компилятор С# испускает инструкцию 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
для не виртуального метода, это создает проблемы безопасности типов?
Ответы
Ответ 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
.