среда, 25 марта 2015 г.

Struct:Interface = Boxing?

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);
}

image


Сами методы при этом будут выглядеть следующим образом:


image


В самих методах мы можем заметить, что версии с параметром интерфейсного типа, вызываются при помощи инструкции callvirt, а метод, принимающий конкретный тип ValueHolder - использует call для вызова Report(). Не безосновательным будет предположение, что виртуальный вызов метода в конечном счёте всё-таки потребует упаковки. Ведь что бы вызвать метод виртуально, нам нужно получить указатель на “таблицу методов типа” (MethodTablePointer). Но у не упакованных структур нет указателя, у них есть только сами данные (экземплярные поля).



Так как же тогда выполняется callvirt инструкция для структур? Всё дело в инструкции constrained !!T в методе ReportGood (строка IL_0003).  Report_Good – это шаблонный метод и для каждого типа принимаемого параметра машинный код будет заново сгенерирован JIT-ом. А так как на этапе генерации JIT уже знает конкретный тип параметра, то он сможет для структур вызвать метод не виртуально, т.е. вшить конкретную реализацию (ведь все структуры sealed и не смыла ожидать override в наследнике).

Комментариев нет:

Отправить комментарий