Boxing и unboxing. Что это и почему это плохо?

Упаковка и распаковка значимых типов в C# — доступный для программиста механизм преобразования размерных типов данных языка C# из значимых в ссылочные и обратно через задействование свойств фундаментального базового класса Object.

Как правило, значимые типы предпочтительнее для использования, чем ссылочные: для них не нужно заботиться о динамическом выделении памяти из «кучи», на них не расходуются ресурсы при сборке мусора, по отношению к ним нет необходимости использовать адресацию через указатели. Однако, иногда необходимо использовать значимые типы совместно со ссылочными[1]. Помимо этого, для использования ряда библиотечных классов нередко возникает нужда в ссылках на экземпляры значимых типов, что заставляет применять механизм их упаковки (англ.boxing) с перспективой распаковки (англ.unboxing) в будущем[2].

Упаковка

В языке C# упаковка работает как неявное преобразование экземпляра любого размерного типа в объект базового класса Object, которое происходит, когда компилятор наталкивается на значимый тип в том контексте, где ожидается появление ссылки. Это преобразование автоматически производится библиотекой CLR причём его выполнение не зависит от того, какой именно тип передан как входной — ссылочный или значимый[3].

Трансформация значимого типа в ссылочный осуществляется автоматически, для этого выполняются следующие шаги[2]:

  1. выделение необходимых ресурсов памяти для размещения нового объекта в «куче». Объёмы этой памяти определяются длиной упаковываемого типа и размером двух служебных членов. Первый из них является указателем на объект-тип, второй — индексом SyncBlockIndex. Этими метаданными снабжаются любые объекты в области динамической памяти.
  2. поля значимого типа копируются в динамически выделенную память.
  3. возвращается адрес полученного объекта в виде ссылочного типа.

Распаковка

Распаковка ссылочного типа в значимый подразумевает, что это должно быть выполнено явно. При этом, необходимо во-первых, сначала удостовериться, что тип упакованного объекта по ссылке соответствует исходному, а во-вторых, скопировать поля данных упакованного объекта в новую переменную данного типа. Как правило, проверку соответствия типов осуществляют с помощью механизма генерирования и обработки исключений[3], после чего копирование переносит внутренние данные (поля) объекта из «кучи» в стек выполняемого приложения, где хранятся его локальные переменные. Последовательность конкретных действий сводится к следующим шагам[2]:

  1. если служебный указатель на упакованный значимый тип имеет значение null, то генерируется исключение NullReferenceException,
  2. если упакованный объект не соответствует требуемому типу, то выбрасывается исключение InvalidCastException.

Отмечается, что распаковка не является противоположностью упаковки в строгом смысле этого слова, она гораздо менее ресурсоёмка если состоит только в запросе указателя на исходный значимый тип. Однако, в большинстве встречающихся приложений, невозможно обойтись без корректного копирования полей объекта из «кучи» в стек, что может крайне отрицательно сказаться на производительности[2].

В связи с этим отмечается, что в некоторых диалектах C++, таких как, например C++/CLI предусмотрены встроенные средства для упаковки значимого типа данных не прибегая к созданию копии его полей[2].

Упаковка — это процесс сохранения значения простого типа (int, char, double …) в экземпляре объекта (object). Распаковка — это процесс вытягивания упакованного значения (int, double, char …) из объекта (object). Следующий пример демонстрирует различие между этими терминами:

object a;

// тип int упаковывается в тип object
a = 300; // упаковка: object <= int

int b;
b = (int)a; // распаковка: int <= object
Какая разница между использованием обобщений и приведением к типу object? Демонстрация преимуществ применения обобщений. Пример

В программах на C# можно объявлять ссылки на тип Object, обращаясь к именам object или Object. Благодаря наследованию, переменным типа Object может быть присвоено значение любых унаследованных типов

Исходя из вышесказанного, можно сделать вывод, что использование типа Object может заменить обобщения. Тогда возникает резонный вопрос: зачем использовать обобщения, если они целиком могут быть заменены типом object?

Использование обобщений вместо использования типа object дает следующие преимущества:

  • отсутствие явного приведения типа в операторе присваивания при использовании обобщений;
  • обеспечение типовой безопасности. Ошибка неправильного приведения типов генерируется уже на этапе компиляции а не на этапе выполнения программы;
  • повышение производительности. Для типа object операция присваивания выполняется дольше, поскольку происходит упаковка, распаковка.

Преимущество 1. Отсутствие явного приведения типа

Если используется обобщение, то не нужно выполнять явное приведение типов в операции присваивания как показано на рисунке 1.

C#. Обобщения. Отличие в явном приведении к типу int между обобщением и типом object
Преимущество 2. Обеспечение типовой безопасности в обобщениях

При использовании класса object в качестве типа можно допустить ошибку, которая на этапе компиляции не будет обнаружена. Эта ошибка окажется на этапе выполнения, что неприемлемо.

На рисунке 2 реализованы такие же классы как на рисунке 1. Однако, в функции main(), для обоих классов осуществляется попытка установить значение типа double.

В случае с классом ObjectClass ошибки на этапе компиляции не возникает. Эта ошибка вызовет исключительную ситуацию на этапе выполнения.

В случае с классом GenClass<T> ошибка будет определена на этапе компиляции. Это связано с тем, что создается типизированный код с привязкой к типу int. В этом коде ошибки определяются на этапе компиляции. Это является основным преимуществом обобщений, которые повышают типовую безопасность.

C#. Обобщения. Особенности выявления ошибки компилятором
Преимущество 3. Повышение производительности

Использование обобщенных классов дает большую производительность (быстродействие) по сравнению с необобщенными. При присвоении значения типа object другим типам и наоборот, компилятор выполняет упаковку и распаковку (смотрите п. 1). Этот процесс требует больше временных затрат чем использование обобщений. В случае с обобщениями формируется типизированный код с привязкой к конкретному типу, который выполняется быстрее.

Рисунок 3 отражает объявление двух классов ObjectClass и GenClass. В функции main() выделены фрагменты кода, в которых проявляется различие в производительности между объектами (object) и обобщениями.

C#. Обобщения. Производительность

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *