interface IMyInterface { }
struct MyStruct : IMyInterface { }
class MyClass
{
public void Method(IMyInterface value){ }
}
Вопрос №1: Будет упаковка структуры при передачи в метод Method()?
Вопрос №2: Как избежать упаковки?
Что бы код выше обрёл некий смысл, давайте представим следующую ситуацию. Нам нужно, что бы определённые типы в нашем приложении могли вывести на консоль отчёт о своём состоянии. Для этого создадим простой интерфейс ICanReport и реализуем его в одном из типов:
interface ICanReport
{
void Report();
}
struct ValueHolder : ICanReport
{
private readonly int _value;
public ValueHolder(int value)
: this()
{
_value = value;
}
public void Report()
{
Console.WriteLine("My value is: {0}", _value);
}
}
Теперь давайте напишем метод, который будет принимать тип, поддерживающий возможность построения отчёта:
static void Report_Bad1(ICanReport a)
{
a.Report();
}
Если в GetSmaller_Bad1 передать значимый тип, то он сначала будет упакован и только потом ссылка на него будет передана методу. “Это не есть хорошо!”, скажите вы, и будете правы. Что бы побороть эту проблему, мы можем явно указать, что a является структурой и избежать упаковки.
static void Report_Bad2(ValueHolder a)
{
a.Report();
}
Но тут возникает вопрос: “А как же быть с полиморфизмом? Неужели нам для каждого значимого типа придется писать свой метод?”. Тут мы можем вспомнить, что как раз в таких случаях нам может помочь шаблонный метод:
static void Report_Good<T>(T a)
where T : ICanReport
{
a.Report();
}
Но погодите, ведь мы же опять указали, что T является интерфейсным типом. Не будет ли это опять приводить к упаковке? К счастью ответ - “НЕТ!” и вот почему. Когда мы вызываем шаблонный метод, то компилятор знает, что упаковки можно избежать, так как в рантайме JIT создаст нужную строготипизированную версию нашего метода, в котором входной параметр будет нужного типа (ValueHolder). В итоге передача параметра в метод Report_Bad2 и Report_Good будет происходить одинаково.
static void Main()
{
var value = new ValueHolder(5);
Report_Bad1(value);
Report_Bad2(value);
Report_Good(value);
}
Сами методы при этом будут выглядеть следующим образом:
В самих методах мы можем заметить, что версии с параметром интерфейсного типа, вызываются при помощи инструкции callvirt, а метод, принимающий конкретный тип ValueHolder - использует call для вызова Report(). Не безосновательным будет предположение, что виртуальный вызов метода в конечном счёте всё-таки потребует упаковки. Ведь что бы вызвать метод виртуально, нам нужно получить указатель на “таблицу методов типа” (MethodTablePointer). Но у не упакованных структур нет указателя, у них есть только сами данные (экземплярные поля).
Так как же тогда выполняется callvirt инструкция для структур? Всё дело в инструкции constrained !!T в методе ReportGood (строка IL_0003). Report_Good – это шаблонный метод и для каждого типа принимаемого параметра машинный код будет заново сгенерирован JIT-ом. А так как на этапе генерации JIT уже знает конкретный тип параметра, то он сможет для структур вызвать метод не виртуально, т.е. вшить конкретную реализацию (ведь все структуры sealed и не смыла ожидать override в наследнике).
Комментариев нет:
Отправить комментарий